webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
React · Lesson 7 of 7

Server state and TanStack Query

Why server data isn't useState, query keys, staleTime and invalidation.

Updated

What server state is

Server state = data that lives on the server (in the database, in an API), while you only have a copy in the browser: users, products, orders, comments. The opposite is client state — data that exists only in the UI: an open modal, the text in an input, the theme.

The difference isn't academic — the two have completely different problems:

Client state Server state
Examples the theme, an open modal, a wizard step users, products, orders
Owner you (the UI) the server — you have a copy
Synchronous yes no — it comes over the network
Can go stale no at any time (another user changed it)
Problems where to keep it loading, errors, caching, deduplication, refetching, invalidation, pagination
Tools useState, the URL, Zustand Server Components, TanStack Query, SWR

The classic mistake: server data in useState + useEffect. You end up reimplementing — badly — caching, loading, retries, deduplication.

Option 1: Server Components (Next.js)

You fetch the data on the server and render the HTML directly. There's no loading state on the client, no cache to keep in sync in the browser.

export default async function Page() {
  const posts = await getPosts()        // cached with 'use cache' + cacheTag('posts')
  return <PostList posts={posts} />
}

A mutation → a Server Action → updateTag('posts') → the page re-renders with fresh data.

Option 2: TanStack Query (on the client)

A library that manages server state in the browser: caching, refetching, deduplication, retries, pagination, mutations.

const { data, isPending, error } = useQuery({
  queryKey: ['posts', { page }],                       // the data's identity in the cache
  queryFn: () => api(`/api/posts?page=${page}`),
  staleTime: 60_000,                                   // fresh for 1 min → no refetch
})

const queryClient = useQueryClient()
const createPost = useMutation({
  mutationFn: (post: NewPost) => api('/api/posts', { method: 'POST', body: JSON.stringify(post) }),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
})

The key concepts

Concept What it means
queryKey an array that identifies the data; everything that affects the result goes into the key (page, filters)
staleTime how long the data is "fresh"; the default 0 = always considered stale
gcTime how long it stays in the cache after nobody uses it (5 min by default)
invalidateQueries marks it stale → refetch for what's on screen; matching by prefix
deduplication 3 components ask for ['user', 1] → a single request
automatic refetch on window focus, on reconnect, on mount (if stale)

Optimistic updates

You show the result before the server responds, so the UI feels instant; if it fails, you roll back.

Where How
React 19 / Server Actions useOptimistic
TanStack Query onMutate (update the cache) + onError (rollback)

How to choose

Situation The choice
Next pages that display data (a blog, a catalog, a profile) Server Components
mutations from forms in Next Server Actions + updateTag
a very interactive UI with server data: dashboards, infinite scroll, polling, live filters TanStack Query (you can prefetch on the server)
an SPA without its own server TanStack Query
pure UI state useState / the URL / Zustand

Summary

  • Server state = a local copy of data from the server; it can go stale at any time.
  • Don't put it in useState + useEffect.
  • Next: Server Components + Server Actions; on the client: TanStack Query with queryKey, staleTime, invalidation.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.