React Query for CMS Data
Decoupling a CMS from its frontend pushes data freshness, cache coordination, and update propagation onto the client. React Query (TanStack Query) handles that by treating server state as a first-class concern: request deduplication, background sync, and predictable cache lifecycles without manual reducers. Within broader Data Fetching & Caching Strategies, it’s the pragmatic default for REST/JSON CMS payloads.
Query keys and configuration
Setup hinges on query keys and timing. CMS endpoints return structured JSON with metadata, pagination cursors, and nested relations. Encode resource type, identifier, and parameters into a deterministic key array so the cache stays consistent and invalidation can target a single resource.
import { useQuery, QueryClient } from '@tanstack/react-query';
const fetchCMSResource = async (endpoint, params) => {
const res = await fetch(`${endpoint}?${new URLSearchParams(params)}`);
if (!res.ok) throw new Error(`CMS fetch failed: ${res.status}`);
return res.json();
};
export function useArticle(id, locale = 'en-US') {
return useQuery({
queryKey: ['cms', 'articles', id, { locale }],
queryFn: () => fetchCMSResource('/api/cms/articles', { id, locale }),
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes
refetchOnWindowFocus: false,
refetchOnReconnect: true,
});
}
staleTime sets how long data stays fresh before a background refetch is eligible; a 5–15 minute window matches most publishing cadences without needless requests. refetchOnWindowFocus: false avoids hammering CMS rate limits during long sessions. gcTime should retain recently visited routes in memory so back-navigation is instant.
Stale-while-revalidate
Serving cached content immediately and refetching in the background keeps perceived performance high even when data has changed. A query moves through these states as staleTime and events drive it:
stateDiagram-v2 [*] --> Fetching: first mount Fetching --> Fresh: data resolved Fresh --> Stale: staleTime elapses Stale --> Refetching: refetch trigger or webhook invalidate Refetching --> Fresh: payload changed, re-render Refetching --> Stale: payload unchanged Stale --> [*]: gcTime evicts unused query
This mirrors SWR Stale-While-Revalidate Patterns but gives finer control over retry logic, pagination merging, and invalidation triggers. On navigation, React Query serves the cached article instantly, validates against the API, and re-renders only if the payload changed — never blocking the render thread. It’s the HTTP caching model from the MDN HTTP Caching reference, applied at the component layer.
Invalidation and editorial workflows
Published changes must propagate without manual cache busting. Combine webhook events with queryClient.invalidateQueries() in a centralized handler that maps CMS webhooks (Contentful, Sanity, Strapi) to targeted updates:
export const handleCMSWebhook = async (queryClient, payload) => {
const { type, id, locale } = payload;
// Invalidate the specific resource
await queryClient.invalidateQueries({
queryKey: ['cms', type, id, { locale }],
refetchType: 'active',
});
// Invalidate list queries when pagination cursors shifted
if (payload.isListUpdate) {
await queryClient.invalidateQueries({
queryKey: ['cms', `${type}-list`, { locale }],
});
}
};
For polling and exponential backoff during live-editing sessions, see React Query background refetch strategies for CMS. The TanStack Query docs cover invalidateQueries, refetchQueries, and setQueryData in full.
Payload shaping
CMS responses carry redundant nested objects, draft states, and localization trees that bloat client memory. Flatten and normalize payloads before they hit the cache to avoid referential inequality and wasted re-renders. Add TypeScript generics for type inference across hooks and enforce schema validation at the fetch layer.
For GraphQL backends, weigh React Query against Apollo Client GraphQL Caching, which normalizes an entity store automatically — React Query stays optimal for REST/JSON given its smaller footprint and explicit cache boundaries. For field extraction and memory-efficient normalization, see Optimizing React Query for headless CMS payloads.
Production checklist
- Use query-key factories: centralize key generation to prevent collisions across locales, environments, and content types.
- Align
staleTimewith editorial cadence: 5–15 minutes based on publishing frequency, not arbitrary defaults. - Invalidate by key, not in bulk: replace blanket clears with webhook-driven
invalidateQueriesscoped to specific resources. - Normalize nested relations: cache reusable blocks (authors, categories, media) independently to cut duplication.
- Disable needless refetches: turn off
refetchOnWindowFocusandrefetchOnMountwhere they don’t earn their cost. - Validate at runtime: use
zodorio-tsinsidequeryFnto catch malformed CMS responses before they reach the UI.