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.
Frontend state comes in two very different kinds.
| Client state | Server state | |
|---|---|---|
| Examples | Is the menu open, form input, selected tab, theme | Products, the current user, orders, comments |
| Owner | This browser tab | The server, where other users and processes change it |
| Always up to date? | Yes, you're the only writer | No, it's a snapshot that starts going stale right away |
| Tools | useState, useReducer, context, URL params | A 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:
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
staleTimeper 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.