Data Fetching
Fetch, cache, and synchronize server data in React with modern libraries and SSR patterns.
Fetching in useEffect
The classic pattern: useEffect triggers fetch on mount or dependency change, sets loading/error/data state. AbortController cancels in-flight requests on cleanup or dependency change.
This pattern duplicates logic across components. Prefer extracting to custom hooks or dedicated data libraries. Never fetch in render — side effects belong in effects or event handlers.
- Fetch in effect, not render
- AbortController on cleanup
- Extract to custom hooks
useEffect(() => {
let cancelled = false;
fetch(url).then(r => r.json()).then(data => {
if (!cancelled) setData(data);
});
return () => { cancelled = true; };
}, [url]);TanStack Query and SWR
TanStack Query (React Query) manages caching, background refetch, stale-while-revalidate, deduplication, and pagination. Query keys identify cache entries. Mutations invalidate related queries.
SWR offers a lighter API with similar stale-while-revalidate semantics. Both eliminate boilerplate loading/error state and prevent duplicate requests across component trees mounting simultaneously.
- Cache keyed by query key
- Stale-while-revalidate UX
- Mutations invalidate cache
const { data, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});Server Components and SSR
Next.js Server Components fetch on the server during render — no client bundle cost for fetch logic, no loading spinner waterfall if structured with Suspense boundaries. Pass serializable data as props to Client Components.
For SPAs, SSR frameworks hydrate fetched data via props or embedded JSON. Align fetch location (server vs client) with freshness requirements and secret handling — API keys stay on server.
- Server fetch during render in RSC
- Serializable props to client
- Secrets never in client fetch
Pagination and Infinite Scroll
Offset pagination is simple but inconsistent under concurrent writes. Cursor pagination uses opaque cursors for stable pages. TanStack Query `useInfiniteQuery` manages page accumulation and fetchNextPage.
Virtualize long lists (react-window, tanstack-virtual) when rendering thousands of rows — fetching more data without virtualization still hurts DOM performance.
- Cursor pagination for stability
- useInfiniteQuery for accumulation
- Virtualize large lists
Error and Loading UX
Model query states explicitly: loading, error, empty, success. Skeleton screens beat spinners for layout stability. Retry buttons on transient errors. Toast notifications for background mutation failures.
Error boundaries catch render errors; query libraries handle fetch errors separately — do not conflate the two. Log errors to monitoring with request context for production debugging.
- Distinct loading, error, empty states
- Skeletons over spinners
- Separate fetch errors from render errors