Concept · Building apps

Server state vs client state

Data you fetched is a cache of someone else’s truth; treat it like one instead of copying it into useState.

3 minTanStack QueryReact Querystale-while-revalidateSWR

Frontend state comes in two very different kinds.

Client stateServer state
ExamplesIs the menu open, form input, selected tab, themeProducts, the current user, orders, comments
OwnerThis browser tabThe server, where other users and processes change it
Always up to date?Yes, you're the only writerNo, it's a snapshot that starts going stale right away
ToolsuseState, useReducer, context, URL paramsA server-state cache (TanStack Query, SWR, framework loaders)

Putting server data in useState with a useEffect fetch works for a demo. Then the real problems arrive, one by one: loading and error states, two components fetching the same thing, refetching after a mutation, race conditions when the ID changes mid-request, stale data after the tab sleeps, retries and pagination. Each one is a cache problem.

A server-state cache

Libraries like TanStack Query treat every request as a cache entry, keyed by what was asked for:

tsx
const { data, isPending, error } = useQuery({
  queryKey: ['orders', customerId],
  queryFn: () => api.listOrders(customerId),
  staleTime: 30_000,
})

const queryClient = useQueryClient()
const cancel = useMutation({
  mutationFn: api.cancelOrder,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['orders'] }),
})

That one hook gives you:

  • Deduplication: ten components asking for the same key make one request.
  • Stale-while-revalidate: show cached data instantly and refetch in the background when it's stale, on mount, window focus or reconnect.
  • Invalidation after writes: a mutation marks related keys stale and they refetch. This is the frontend version of cache invalidation.
  • Race safety: responses for an old key never overwrite the new one.

Habits

  • Don't copy query data into useState. Read it from the cache and derive what you need.
  • Choose staleTime per query. Make it long for reference data and short (or 0) for live data.
  • Optimistic updates improve perceived speed, but you need to roll them back on error. Use them where failures are rare.
  • What's left is usually small. Once server state lives in a cache, the remaining client state rarely needs a global store.

The trade-offs are the same as server-side caching patterns: how stale is acceptable, and who invalidates.

Learn it in depth