DevToolBoxฟรี
บล็อก

React Query Patterns 2026: Data Fetching, Caching และ Mutations ด้วย TanStack Query

13 นาทีโดย DevToolBox

TanStack Query คือโซลูชันมาตรฐานสำหรับการจัดการ server state ใน React applications

useQuery: การดึงและ Cache ข้อมูล

useQuery subscribe server state โดยใช้ queryKey เป็น cache key

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: สร้าง อัพเดต ลบ

useMutation จัดการ CRUD operations ด้วย onSuccess callbacks

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>
    );
}

Optimistic Updates สำหรับ UI แบบ Instant

Optimistic updates ใช้การเปลี่ยนแปลงกับ UI ก่อนที่ server จะตอบกลับ

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'] });
        },
    });
}

Infinite Queries สำหรับ Pagination

useInfiniteQuery จัดการ pagination แบบ cursor-based

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>
    );
}

รูปแบบ Query Key และ Prefetching

Query key factories ที่สม่ำเสมอป้องกัน cache mismatches

// 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 โซลูชันอื่นๆ

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

แนวปฏิบัติที่ดีที่สุด

  • ใช้ query key factories เพื่อความสม่ำเสมอ
  • ตั้งค่า staleTime ตามความต้องการด้านความใหม่ของข้อมูล
  • ให้ loading และ error states เสมอ
  • ใช้ optimistic updates สำหรับ actions ที่รวดเร็ว
  • Prefetch ข้อมูลเมื่อ hover หรือ route transitions

คำถามที่พบบ่อย

staleTime กับ gcTime ต่างกันอย่างไร?

staleTime ควบคุมว่าข้อมูลจะถือว่าเก่าเมื่อใด gcTime ควบคุมว่าจะเก็บใน memory นานแค่ไหน

TanStack Query จัดการ race conditions อย่างไร?

จะ deduplicate requests ที่เหมือนกันโดยอัตโนมัติ

ควรใช้ TanStack Query กับ Redux หรือ Zustand ไหม?

เสริมกัน: TanStack Query สำหรับ server state, Zustand สำหรับ client state

จะจัดการ authentication errors แบบ global ได้อย่างไร?

ใช้ global onError callback ของ QueryClient

TanStack Query v5 มีการเปลี่ยนแปลงอะไรบ้าง?

cacheTime เปลี่ยนชื่อเป็น gcTime, queryFn parameter เป็น object, รองรับ RSC

เครื่องมือที่เกี่ยวข้อง

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

{ }JSON FormatterB→Base64 Encode OnlineIDUUID Generator Online

บทความที่เกี่ยวข้อง

React Server Components: คู่มือฉบับสมบูรณ์ 2026

เชี่ยวชาญ React Server Components: สถาปัตยกรรม, การดึงข้อมูล, streaming และการย้าย

TypeScript Type Guards: คู่มือการตรวจสอบประเภทขณะรันไทม์

เชี่ยวชาญ TypeScript type guards: typeof, instanceof, in และ guard แบบกำหนดเอง

Next.js App Router: คู่มือการย้ายระบบฉบับสมบูรณ์ 2026

เชี่ยวชาญ Next.js App Router ด้วยคู่มือครบถ้วน เรียนรู้ Server Components, การดึงข้อมูล, เลย์เอาต์, สตรีมมิ่ง, Server Actions และการย้ายจาก Pages Router ทีละขั้นตอน