React Query with Next.js

When You Still Fetch on the Client

Most reads should happen in Server Components. Client-side fetching is still useful for data that changes while the page is open: live order status, infinite scroll, polling, autocomplete. TanStack Query (React Query) gives caching, retries and refetching.

// app/providers.tsx
'use client';

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

export default function Providers({ children }: { children: React.ReactNode }) {
  const [client] = useState(() => new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } }));
  return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}

Polling Order Status

'use client';

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

type Status = { orderId: string; status: 'PLACED' | 'PACKED' | 'SHIPPED' | 'DELIVERED'; eta: string };

export default function OrderStatus({ orderId, initial }: { orderId: string; initial: Status }) {
  const { data, isFetching } = useQuery({
    queryKey: ['order-status', orderId],
    queryFn: async (): Promise<Status> => {
      const res = await fetch('/api/orders/' + orderId + '/status');
      if (!res.ok) throw new Error('Failed');
      return res.json();
    },
    initialData: initial, // rendered by the Server Component, no loading flash
    refetchInterval: (q) => (q.state.data?.status === 'DELIVERED' ? false : 15_000),
  });

  return (
    <p>
      {data.status} — ETA {data.eta} {isFetching && '(updating)'}
    </p>
  );
}

Tip: Pass server-fetched data as initialData (or use hydration with HydrationBoundary) so the first paint has real content.

Choosing a Tool

StateBest place
Server data for the pageServer Component (+ cache)
Filters, sort, page, tabURL search params
Form inputLocal useState / form actions
Cart, theme, logged-in user (UI)Context or Zustand
Live / polling / infinite listsReact Query