Home > Blog > tech

TanStack Query (React Query) คืออะไร? Data Fetching ที่ดีที่สุดสำหรับ React 2026

TanStack Query React Data Fetching 2026
2026-04-16 | tech | 3500 words

การ Fetch ข้อมูลจาก API ใน React เป็นเรื่องที่ทุกโปรเจคต้องทำ แต่ถ้าจัดการเอง (useEffect + useState) จะเจอปัญหามากมาย ทั้ง Loading state, Error handling, Caching, Background refetching, Race conditions, Stale data ทั้งหมดนี้แก้ได้ด้วย TanStack Query

บทความนี้จะสอนใช้ TanStack Query (เดิมชื่อ React Query) ตั้งแต่พื้นฐาน จนถึง Advanced patterns ที่ใช้จริงใน Production ปี 2026

TanStack Query คืออะไร?

TanStack Query (v5) คือ Library สำหรับจัดการ Server State ใน React/Vue/Solid/Angular ทำหน้าที่ Fetch, Cache, Synchronize, และ Update data จาก Server โดยอัตโนมัติ

ทำไมต้องใช้ TanStack Query?

// ❌ วิธีเดิม — useEffect + useState (ปัญหาเยอะ)
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setUsers(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });
    return () => { cancelled = true; };
  }, []);

  // ปัญหา:
  // - ไม่มี Caching (fetch ใหม่ทุกครั้งที่ mount)
  // - ไม่มี Background refetching
  // - Race condition ต้องจัดการเอง
  // - Loading/Error state boilerplate ทุก Component
  // - ไม่มี Retry logic
  // - ไม่มี Pagination support
}

// ✅ TanStack Query — สะอาด ครบ จบ
function UserList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
  });

  // ✅ Caching อัตโนมัติ
  // ✅ Background refetching
  // ✅ Race condition handled
  // ✅ Loading/Error state ในตัว
  // ✅ Retry 3 ครั้งอัตโนมัติ
  // ✅ Stale-while-revalidate
}

ติดตั้ง TanStack Query

# ติดตั้ง
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools  # DevTools (แนะนำ)

# Setup ใน App
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,    // 5 นาที (data ถือว่า fresh)
      gcTime: 1000 * 60 * 30,      // 30 นาที (เก็บ cache)
      retry: 3,                     // retry 3 ครั้งถ้า fail
      refetchOnWindowFocus: true,   // refetch เมื่อ focus กลับมา
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

useQuery — ดึงข้อมูล

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

// Basic useQuery
function UserProfile({ userId }) {
  const { data, isLoading, isError, error, isFetching } = useQuery({
    queryKey: ['user', userId],          // Query Key — unique identifier
    queryFn: () =>                       // Query Function — fetch data
      fetch(`/api/users/${userId}`).then(r => r.json()),
    staleTime: 1000 * 60 * 5,           // 5 นาที
    gcTime: 1000 * 60 * 30,             // 30 นาที (เดิมชื่อ cacheTime)
    enabled: !!userId,                   // ไม่ fetch ถ้า userId เป็น null/undefined
    select: (data) => data.user,         // Transform data ก่อนส่งให้ Component
    placeholderData: { name: 'Loading...' }, // Placeholder ระหว่างรอ
  });

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

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
      {isFetching && <span>Refreshing...</span>}
    </div>
  );
}

Query Keys — หัวใจของ TanStack Query

// Query Key คือ Array ที่ใช้ระบุ Query แต่ละตัว
// TanStack Query ใช้ Query Key เพื่อ:
// 1. Caching — key เดียวกัน = ใช้ cache เดียวกัน
// 2. Refetching — invalidate ตาม key
// 3. Deduplication — 2 component ใช้ key เดียวกัน = fetch ครั้งเดียว

// ตัวอย่าง Query Keys:
useQuery({ queryKey: ['users'], queryFn: fetchUsers });
useQuery({ queryKey: ['users', 'active'], queryFn: fetchActiveUsers });
useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId) });
useQuery({ queryKey: ['users', { page, sort, filter }], queryFn: fetchUsers });

// ⚠ Key เปลี่ยน = fetch ใหม่อัตโนมัติ!
// เช่น userId เปลี่ยนจาก 1 → 2 → fetch user 2 ทันที

Caching — staleTime vs gcTime

Settingชื่อเดิมหน้าที่Default
staleTimestaleTimeข้อมูลถือว่า "fresh" นานแค่ไหน (ไม่ refetch)0 (stale ทันที)
gcTimecacheTimeเก็บ cache นานแค่ไหนหลัง Component unmount5 นาที
// staleTime: 0 (Default)
// → ทุกครั้งที่ Component mount → refetch ทันที (แต่แสดง cache ก่อน)
// → เหมาะกับข้อมูลที่เปลี่ยนบ่อย (chat, notifications)

// staleTime: 5 นาที
// → 5 นาทีหลัง fetch → ข้อมูลยังถือว่า fresh → ไม่ refetch
// → เหมาะกับข้อมูลที่ไม่ค่อยเปลี่ยน (user profile, settings)

// staleTime: Infinity
// → ไม่ refetch เลย จนกว่าจะ invalidate เอง
// → เหมาะกับ Static data (country list, categories)

// gcTime: 30 นาที
// → หลัง Component unmount → เก็บ cache ไว้ 30 นาที
// → ถ้ากลับมาภายใน 30 นาที → ใช้ cache ทันที (ไม่ต้อง Loading)
// → หลัง 30 นาที → Garbage collect → ลบ cache

Background Refetching

// TanStack Query refetch อัตโนมัติเมื่อ:
// 1. Window Focus — กลับมาที่ Tab นี้
// 2. Network Reconnect — Internet กลับมา
// 3. Component Mount — Component ถูก render ใหม่ (ถ้า data stale)
// 4. Interval — refetchInterval

useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchOnWindowFocus: true,      // Default: true
  refetchOnReconnect: true,        // Default: true
  refetchOnMount: true,            // Default: true (ถ้า stale)
  refetchInterval: 1000 * 30,      // Refetch ทุก 30 วินาที
  refetchIntervalInBackground: false, // ไม่ refetch ถ้า Tab ไม่ active
});

useMutation — สร้าง/แก้ไข/ลบข้อมูล

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

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

  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
        headers: { 'Content-Type': 'application/json' },
      }).then(r => r.json()),

    onSuccess: (data) => {
      // ✅ Invalidate → refetch users list อัตโนมัติ
      queryClient.invalidateQueries({ queryKey: ['users'] });
      console.log('Created:', data);
    },
    onError: (error) => {
      console.error('Failed:', error.message);
    },
    onSettled: () => {
      // ทำทั้งตอน success และ error
    },
  });

  return (
    <button
      onClick={() => mutation.mutate({ name: 'John', email: 'john@test.com' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Creating...' : 'Create User'}
    </button>
  );
}

Query Invalidation — อัปเดต Cache

const queryClient = useQueryClient();

// Invalidate ทุก query ที่ขึ้นต้นด้วย 'users'
queryClient.invalidateQueries({ queryKey: ['users'] });

// Invalidate เฉพาะ user ID 5
queryClient.invalidateQueries({ queryKey: ['user', 5] });

// Invalidate ทุก query (rare)
queryClient.invalidateQueries();

// Set data ตรงๆ (ไม่ต้อง refetch)
queryClient.setQueryData(['user', 5], (old) => ({
  ...old,
  name: 'Updated Name',
}));

Pagination — useInfiniteQuery

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

function InfiniteUserList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteQuery({
    queryKey: ['users', 'infinite'],
    queryFn: ({ pageParam }) =>
      fetch(`/api/users?page=${pageParam}&limit=20`).then(r => r.json()),
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      // return undefined ถ้าไม่มีหน้าถัดไป
      return lastPage.hasMore ? allPages.length + 1 : undefined;
    },
  });

  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.users.map(user => (
            <UserCard key={user.id} user={user} />
          ))}
        </div>
      ))}

      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? 'Loading more...'
          : hasNextPage ? 'Load More'
          : 'No more users'}
      </button>
    </div>
  );
}

Optimistic Updates — อัปเดตก่อน Server ตอบ

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // 1. Cancel ongoing refetches
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    // 2. Snapshot previous value
    const previousTodos = queryClient.getQueryData(['todos']);

    // 3. Optimistically update
    queryClient.setQueryData(['todos'], (old) =>
      old.map(todo =>
        todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
      )
    );

    // 4. Return context with snapshot
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // ❌ Rollback ถ้า error
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
  onSettled: () => {
    // ✅ Refetch เพื่อให้แน่ใจว่า data ตรงกับ server
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Prefetching — โหลดข้อมูลล่วงหน้า

const queryClient = useQueryClient();

// Prefetch เมื่อ Hover
function UserLink({ userId }) {
  return (
    <a
      href={`/users/${userId}`}
      onMouseEnter={() => {
        queryClient.prefetchQuery({
          queryKey: ['user', userId],
          queryFn: () => fetchUser(userId),
          staleTime: 1000 * 60 * 5,
        });
      }}
    >
      View User
    </a>
  );
}

// Prefetch ใน Router Loader (React Router v7+)
export const loader = (queryClient) => async ({ params }) => {
  await queryClient.ensureQueryData({
    queryKey: ['user', params.userId],
    queryFn: () => fetchUser(params.userId),
  });
  return null;
};

TanStack Query DevTools

// DevTools แสดง:
// - ทุก Query ที่ active
// - สถานะของแต่ละ Query (fresh, stale, fetching, paused, inactive)
// - Data ที่อยู่ใน cache
// - จำนวนครั้งที่ fetch
// - เวลาที่ fetch ล่าสุด
// - Query Key
// - สามารถ Trigger refetch, Invalidate, Reset ได้จาก DevTools

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

// เพิ่มใน App (เฉพาะ Development)
<ReactQueryDevtools
  initialIsOpen={false}
  buttonPosition="bottom-right"
/>

// Production: DevTools จะไม่ถูก include อัตโนมัติ (tree-shaking)

TanStack Query vs SWR vs RTK Query

FeatureTanStack Query v5SWR v2RTK Query
Bundle Size~39 KB~12 KBต้องใช้กับ Redux (~45 KB)
Cachingยืดหยุ่นมากดีดีมาก (normalized)
DevToolsดีเยี่ยมพื้นฐานRedux DevTools
Optimistic UpdatesBuilt-inทำเองได้Built-in
Infinite QueryBuilt-inuseSWRInfiniteทำเองได้
MutationsuseMutationuseSWRMutationBuilt-in (endpoints)
SSR Supportดีมากดีดี
FrameworkReact, Vue, Solid, AngularReact onlyReact only (Redux)
เหมาะกับทุกโปรเจคโปรเจคเล็ก-กลางโปรเจคที่ใช้ Redux อยู่แล้ว
เลือกอย่างไร? ถ้าใช้ Redux อยู่แล้ว → RTK Query, ถ้าต้องการ Lightweight → SWR, ถ้าต้องการ Full-featured + Multi-framework → TanStack Query (แนะนำ)

TanStack Query กับ Next.js SSR

// Next.js App Router + TanStack Query
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

export function Providers({ children }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 1000 * 60 * 5,
      },
    },
  }));

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

// Server Component — Prefetch
// app/users/page.tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';

export default async function UsersPage() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UserList />  {/* Client Component ใช้ cache จาก Server */}
    </HydrationBoundary>
  );
}

Error Handling & Retry

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retry: 3,                    // retry 3 ครั้ง (default)
  retryDelay: (attemptIndex) => // Exponential backoff
    Math.min(1000 * 2 ** attemptIndex, 30000),
  retryOnMount: true,
  throwOnError: false,         // ไม่ throw (ใช้ isError แทน)
});

// Error Boundary (React)
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
          <div>
            <p>Something went wrong</p>
            <button onClick={resetErrorBoundary}>Try again</button>
          </div>
        )}>
          <UserList />
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  );
}

Best Practices

Practiceทำไม
ตั้ง staleTime ให้เหมาะสมลด unnecessary refetch ประหยัด bandwidth
ใช้ Query Key Factoryจัดการ key เป็นระบบ ไม่ผิดพลาด
แยก Query Function ออกจาก ComponentReusable test ง่าย
ใช้ select เพื่อ transform dataComponent ไม่ต้องรู้ structure ของ API
ใช้ Prefetch สำหรับ NavigationUser ไม่ต้อง Loading ตอนเปลี่ยนหน้า
ใช้ Optimistic Updates สำหรับ UX ดีขึ้นUser เห็นผลทันที ไม่ต้องรอ Server
ตั้ง gcTime ให้นานพอกลับมาหน้าเดิมไม่ต้อง Loading ใหม่
ใช้ DevTools ตอน DevelopmentDebug cache, timing, refetch ง่ายมาก
// Query Key Factory Pattern (แนะนำ)
export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id) => [...userKeys.details(), id] as const,
};

// ใช้:
useQuery({ queryKey: userKeys.detail(userId), queryFn: ... });
queryClient.invalidateQueries({ queryKey: userKeys.lists() }); // invalidate ทุก list

สรุป

TanStack Query เป็น Library ที่ทำให้ Data Fetching ใน React เปลี่ยนจาก "ปวดหัว" เป็น "สะดวก" ด้วย useQuery, useMutation, Caching (staleTime/gcTime), Background refetching, Pagination (useInfiniteQuery), Optimistic updates, DevTools และอีกมากมาย

ถ้าโปรเจค React ของคุณมีการ Fetch API จากหลาย Endpoint TanStack Query คือสิ่งที่ควรติดตั้งเป็นอย่างแรก เพราะมันจัดการ Server State ให้ทั้งหมด ให้คุณ Focus กับ UI และ Business Logic แทน


Back to Blog | iCafe Forex | SiamLanCard | Siam2R