GitHub

Server State with React Query

Fetch, cache, and synchronize server data using TanStack Query v5. Covers pagination, search, background refetching, and loading states.

01

Principle

Server state is async, shared, and can become stale. TanStack Query owns the entire lifecycle — fetching, caching, deduplication, and background revalidation. Components declare what data they need via custom hooks and stay completely free of fetch logic.

lightbulb

Never mirror server data into useState. If it came from an API, it belongs in React Query's cache. Local state is only for UI — modals, toggles, input values.

02

Rules

  • check_circle
    Hooks own the fetchingQuery hooks go in hooks/queries/. Components only call the hook and render the result.
  • check_circle
    Hierarchical query keysStructure keys as arrays: ["users", "list", { search, page }] for granular cache invalidation.
  • check_circle
    Always set staleTimeDefault staleTime is 0 — every render refetches. Be explicit: 5 min for lists, 10 min for details.
  • check_circle
    Handle all statesAlways render isLoading, isError, and empty states. Never assume data exists on first render.
03

Pattern

hooks/queries/useUsers.ts
import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
import { getUsers, type GetUsersParams } from '@/services/users';

export function useUsers(params: GetUsersParams = {}) {
  return useQuery({
    queryKey: queryKeys.users.list(params),
    queryFn: () => getUsers(params),
    staleTime: 1000 * 60 * 5,       // 5 minutes
    placeholderData: (prev) => prev, // no layout shift on page change
  });
}
04

Implementation

info

Version Compatibility

Requires React 19+ and the latest stable versions of all dependencies shown.

In Next.js App Router, prefetch data in a Server Component and hydrate the client cache via HydrationBoundary. The user sees real data on first paint — no loading spinner.

app/users/page.tsx
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getQueryClient } from '@/lib/get-query-client';
import { queryKeys } from '@/lib/query-keys';
import { getUsers } from '@/services/users';

export default async function UsersPage() {
  const qc = getQueryClient();
  await qc.prefetchQuery({
    queryKey: queryKeys.users.list({}),
    queryFn: () => getUsers({}),
  });
  return (
    <HydrationBoundary state={dehydrate(qc)}>
      <UserList />
    </HydrationBoundary>
  );
}
05

Live Demo

Loading users...

menu_book
React Patterns

Helping developers build robust React applications since 2025.

© 2025 React Patterns Cookbook. Built with ❤️ for the community.
react-principles