Modern React 31: Data fetching with a cache
This is part 31 of the Modern React development series.
Server data is not the same as local UI state. It has loading states, errors, freshness rules, retries, deduplication, and invalidation. A data cache gives those concerns a shared owner.
Concept
A client data cache stores server responses by query keys and coordinates fetching, refetching, pending state, errors, and sharing across components. React docs point apps toward framework data APIs or purpose-built fetching libraries instead of one-off Effects for every request.
Terms
- Server state: Data owned by a server or external source, not by one React component.
- Query key: A stable identifier for one cached read.
- Cache: A store that can reuse data and coordinate refresh behavior.
- Stale data: Cached data that can be shown but may need a background refresh.
Mental model
Think of the cache as a library desk. Components ask for a book by catalog key. The desk either hands over the copy it has, fetches a fresh copy, or tells the component the request failed.
How it is used
Use a cache for backend data used by multiple components, paginated lists, detail pages, search results, dashboards, and data that needs refetching after mutations or window focus.
How to use it
- Name each read with a stable query key.
- Put the fetch function at the query boundary, not inside unrelated render code.
- Render loading, error, empty, and success states explicitly.
- Use router loaders or server fetching when route navigation should own the request.
- Invalidate or update affected queries after mutations.
Example: TanStack Query read
import { useQuery } from "@tanstack/react-query";import { fetchProject } from "./projects";
type Project = { id: string; name: string };
export function ProjectName({ projectId }: { projectId: string }) { const query = useQuery({ queryKey: ["project", projectId], queryFn: () => fetchProject(projectId), });
if (query.isPending) return <p>Loading project...</p>; if (query.isError) return <p>Project could not load.</p>;
return <h1>{query.data.name}</h1>;}Loading project...
The query key names the cached read. The component renders each state of the request.
Example: Query client provider
import type { ReactNode } from "react";import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export function AppProviders({ children }: { children: ReactNode }) { return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> );}Cached data area
A cache provider gives components access to the same query client instead of each component owning isolated fetch logic.
Details to watch
- Effects: Manual fetch Effects are useful for some integrations, but they do not provide cache behavior by themselves.
- Key design: Keys should include every input that changes which data is fetched.
- Freshness: A cache can show previous data while fetching new data. That is different from local state ownership.
- Framework data: Framework loaders and Server Components can fetch before client components render, which avoids client waterfalls.
Series navigation
- Previous: Part 30: Routing and nested layouts
- Next: Part 32: Mutations and cache invalidation
- Series index: Modern React development
References
- Build a React App from Scratch, data fetching
- You Might Not Need an Effect
- TanStack Query React overview