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.

JavaScript
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:

JavaScript
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

  1. Use query-key factories: centralize key generation to prevent collisions across locales, environments, and content types.
  2. Align staleTime with editorial cadence: 5–15 minutes based on publishing frequency, not arbitrary defaults.
  3. Invalidate by key, not in bulk: replace blanket clears with webhook-driven invalidateQueries scoped to specific resources.
  4. Normalize nested relations: cache reusable blocks (authors, categories, media) independently to cut duplication.
  5. Disable needless refetches: turn off refetchOnWindowFocus and refetchOnMount where they don’t earn their cost.
  6. Validate at runtime: use zod or io-ts inside queryFn to catch malformed CMS responses before they reach the UI.