# Server State with React Query

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

**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.

**Tip:** 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.

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

**Pattern** — `features/examples/hooks/useUsers.ts`

```ts
import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
import { usersService, type GetUsersParams } from '@/lib/services/users';

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

**Implementation — Next.js**

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.

File: `app/users/page.tsx`

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

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

**Implementation — Vite**

In Vite, QueryClientProvider wraps the app. Call the hook directly inside your component — React Query handles loading and error states automatically.

File: `pages/UsersPage.tsx`

```tsx
import { useUsers } from '@/features/examples/hooks/useUsers';
import { LoadingState } from '@/shared/components/LoadingState';

export function UsersPage() {
  const { data, isLoading, isError } = useUsers({ limit: 10, skip: 0 });

  if (isLoading) return <LoadingState rows={5} />;
  if (isError) return <p>Failed to load users.</p>;

  return (
    <ul>
      {data.users.map((user) => (
        <li key={user.id}>{user.name} — {user.email}</li>
      ))}
    </ul>
  );
}
```

Read more: https://reactprinciples.dev/cookbook/server-state

---

Source: https://reactprinciples.dev/nextjs/cookbook/server-state
All recipes (compact): https://reactprinciples.dev/llms.txt
All recipes (full): https://reactprinciples.dev/llms-full.txt
