DevToolBoxFREE
BlogAdvertise

Wzorce React Query 2026: Data Fetching, Caching i Mutations z TanStack Query

13 minby DevToolBox

TanStack Query jest standardowym rozwiązaniem do zarządzania stanem serwera w aplikacjach React.

useQuery: pobieranie i cachowanie danych

useQuery subskrybuje stan serwera z queryKey jako kluczem cache.

import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';

// Setup: wrap your app
const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            staleTime: 1000 * 60 * 5,  // 5 minutes
            gcTime: 1000 * 60 * 30,     // 30 minutes (formerly cacheTime)
            retry: 3,
            refetchOnWindowFocus: true,
        },
    },
});

function App() {
    return (
        <QueryClientProvider client={queryClient}>
            <MyApp />
        </QueryClientProvider>
    );
}

// Basic query
interface User {
    id: number;
    name: string;
    email: string;
}

function useUser(userId: number) {
    return useQuery<User>({
        queryKey: ['user', userId],      // cache key — must be unique
        queryFn: async () => {
            const res = await fetch(`/api/users/${userId}`);
            if (!res.ok) throw new Error('Failed to fetch user');
            return res.json();
        },
        enabled: userId > 0,            // only run if userId is valid
        staleTime: 1000 * 60 * 10,     // override global default
    });
}

function UserProfile({ userId }: { userId: number }) {
    const { data, isLoading, isError, error } = useUser(userId);

    if (isLoading) return <div>Loading...</div>;
    if (isError) return <div>Error: {error.message}</div>;

    return <div>{data.name} — {data.email}</div>;
}

useMutation: tworzenie, aktualizacja, usuwanie

useMutation obsługuje operacje CRUD z callbackami onSuccess.

import { useMutation, useQueryClient } from '@tanstack/react-query';

interface CreatePostInput {
    title: string;
    body: string;
    userId: number;
}

function useCreatePost() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: async (input: CreatePostInput) => {
            const res = await fetch('/api/posts', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(input),
            });
            if (!res.ok) throw new Error('Failed to create post');
            return res.json();
        },
        onSuccess: (newPost) => {
            // Invalidate and refetch posts list
            queryClient.invalidateQueries({ queryKey: ['posts'] });
            // Or add directly to cache (no refetch needed)
            queryClient.setQueryData(['post', newPost.id], newPost);
        },
        onError: (error) => {
            console.error('Failed to create post:', error);
        },
    });
}

function CreatePostForm() {
    const mutation = useCreatePost();

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        mutation.mutate({ title: 'New Post', body: 'Content', userId: 1 });
    };

    return (
        <form onSubmit={handleSubmit}>
            <button type="submit" disabled={mutation.isPending}>
                {mutation.isPending ? 'Creating...' : 'Create Post'}
            </button>
            {mutation.isError && <p>Error: {mutation.error.message}</p>}
            {mutation.isSuccess && <p>Post created!</p>}
        </form>
    );
}

Optymistyczne aktualizacje dla natychmiastowego UI

Optymistyczne aktualizacje stosują zmiany przed odpowiedzią serwera.

import { useMutation, useQueryClient } from '@tanstack/react-query';

interface Todo {
    id: number;
    title: string;
    completed: boolean;
}

function useToggleTodo() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: (todo: Todo) =>
            fetch(`/api/todos/${todo.id}`, {
                method: 'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ completed: !todo.completed }),
            }).then(r => r.json()),

        // Optimistic update — runs BEFORE the mutation
        onMutate: async (updatedTodo) => {
            // Cancel outgoing refetches
            await queryClient.cancelQueries({ queryKey: ['todos'] });

            // Snapshot current data for rollback
            const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

            // Optimistically update the cache
            queryClient.setQueryData<Todo[]>(['todos'], (old) =>
                old?.map(todo =>
                    todo.id === updatedTodo.id
                        ? { ...todo, completed: !todo.completed }
                        : todo
                )
            );

            return { previousTodos }; // context for onError
        },

        // If mutation fails, roll back to snapshot
        onError: (_err, _variables, context) => {
            if (context?.previousTodos) {
                queryClient.setQueryData(['todos'], context.previousTodos);
            }
        },

        // Always refetch after error or success
        onSettled: () => {
            queryClient.invalidateQueries({ queryKey: ['todos'] });
        },
    });
}

Nieskończone zapytania dla paginacji

useInfiniteQuery obsługuje paginację opartą na kursorze.

import { useInfiniteQuery } from '@tanstack/react-query';

interface Page {
    items: Post[];
    nextCursor?: string;
}

function useInfinitePosts() {
    return useInfiniteQuery<Page>({
        queryKey: ['posts', 'infinite'],
        queryFn: async ({ pageParam }) => {
            const url = pageParam
                ? `/api/posts?cursor=${pageParam}`
                : '/api/posts';
            return fetch(url).then(r => r.json());
        },
        initialPageParam: undefined as string | undefined,
        getNextPageParam: (lastPage) => lastPage.nextCursor,
    });
}

function InfinitePostList() {
    const {
        data,
        fetchNextPage,
        hasNextPage,
        isFetchingNextPage,
    } = useInfinitePosts();

    return (
        <div>
            {data?.pages.flatMap(page => page.items).map(post => (
                <div key={post.id}>{post.title}</div>
            ))}
            {hasNextPage && (
                <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
                    {isFetchingNextPage ? 'Loading...' : 'Load more'}
                </button>
            )}
        </div>
    );
}

Wzorce kluczy zapytań i prefetching

Spójne factory kluczy zapobiegają błędom cache.

// Query key patterns — factory functions for consistency
const queryKeys = {
    all: ['posts'] as const,
    lists: () => [...queryKeys.all, 'list'] as const,
    list: (filters: string) => [...queryKeys.lists(), { filters }] as const,
    details: () => [...queryKeys.all, 'detail'] as const,
    detail: (id: number) => [...queryKeys.details(), id] as const,
};

// Usage:
useQuery({ queryKey: queryKeys.detail(1), queryFn: ... });

// Invalidate all posts:
queryClient.invalidateQueries({ queryKey: queryKeys.all });

// Invalidate only lists:
queryClient.invalidateQueries({ queryKey: queryKeys.lists() });

// Prefetching for better UX
async function prefetchUser(userId: number) {
    await queryClient.prefetchQuery({
        queryKey: ['user', userId],
        queryFn: () => fetchUser(userId),
        staleTime: 10 * 60 * 1000, // only prefetch if > 10 min stale
    });
}

// On hover: prefetch before click
<link onMouseEnter={() => prefetchUser(userId)} href={`/users/${userId}`}>
    View Profile
</link>

TanStack Query vs inne rozwiązania

FeatureTanStack QueryRedux ToolkitSWRApollo
CachingBuilt-in (staleTime, gcTime)ManualBuilt-inBuilt-in (Apollo-only)
Optimistic updatesFirst-classManualBasicFirst-class
Infinite queryuseInfiniteQueryManualuseSWRInfinitefetchMore
MutationsuseMutationRTK QueryNo (use fetch)useMutation
DevToolsExcellentExcellentBasicGood
REST + GraphQLBothBothREST focusedGraphQL only

Najlepsze praktyki

  • Używać factory kluczy zapytań dla spójności.
  • Ustawiać staleTime zgodnie z wymaganiami świeżości danych.
  • Zawsze dostarczać stany ładowania i błędu.
  • Używać optymistycznych aktualizacji dla szybkich akcji.
  • Pobierać dane z wyprzedzeniem przy hover lub przejściach tras.

FAQ

Różnica między staleTime a gcTime?

staleTime kontroluje, kiedy dane są przestarzałe; gcTime jak długo pozostają w pamięci.

Jak TanStack Query obsługuje race conditions?

Automatycznie deduplikuje identyczne żądania.

TanStack Query z Redux czy Zustand?

Uzupełniające: TanStack Query dla stanu serwera, Zustand dla stanu klienta.

Globalna obsługa błędów uwierzytelniania?

Użyć globalnego callbacka onError QueryClient.

Zmiany w TanStack Query v5?

cacheTime przemianowany na gcTime, parametr queryFn jako obiekt, wsparcie RSC.

Czy to było pomocne?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy