# 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.

Concept · Building apps · https://levelcode.org/concepts/server-vs-client-state

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:

```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](https://levelcode.org/concepts/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](https://levelcode.org/concepts/derived-state) 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](https://levelcode.org/concepts/caching-patterns): how stale is acceptable, and who invalidates.
