React State Management in Depth #1: Client State vs Server State
In the React Basics course (#1–#15) we covered useState and useReducer, lifting state up, and useContext. For a small app, these built-in tools are plenty. But as an app grows, you eventually hit a point where useState and useContext alone start to feel like a struggle. This series begins right at that point.
It runs in six parts.
- #1 Client state vs server state ← this post
- #2 Server state with TanStack Query
- #3 Lightweight client state with Zustand
- #4 Jotai and the atom model
- #5 Redux Toolkit and the legacy context
- #6 Which tool, when — a decision guide
This post has almost no code. Before picking a tool, it’s essential to first settle one fact: there are two kinds of state with different natures. Getting this straight is what makes the other five parts click.
When the built-in tools start to strain #
React ships with three built-in state tools.
useState— local state for a single componentuseReducer— local state with complex transition rulesuseContext— passing a value across the component tree
As an app grows, you start using these tools to solve two very different problems at once. And the two problems are nothing alike.
Problem A. Values like the dark mode setting, whether the sidebar is open, modal visibility, or the items in a cart — values that are born in the browser and only mean something inside the browser — need to be shared across several components. You can solve it with useContext, but every time the value changes the whole tree under the Provider tends to re-render, and as Contexts multiply, the Provider nesting gets deeper.
Problem B. You have to deal with values like a user list, posts, or order history — values whose real source of truth lives in a server database, where the screen only shows a copy. The familiar pattern is to fetch inside useEffect and stash the result in useState, but once you hand-roll loading indicators, error handling, refetching, caching, and keeping multiple screens in sync, the code quickly grows complicated.
Here’s the key. Problem A and Problem B are both handled with the same useState, yet they are completely different kinds of state. If you can’t tell them apart, your tool choices will keep missing the mark.
Client state and server state #
Let’s split state into these two kinds.
| Aspect | Client state | Server state |
|---|---|---|
| Where the source of truth lives | The browser (this state is the source) | The server database (the screen is a copy) |
| Examples | Dark mode, modal open, form input, selected tab | User list, posts, order history, search results |
| Needs syncing | No (self-contained in my browser) | Yes (must keep up with the server) |
| Freshness | Always current (I’m the one changing it) | Goes stale over time (others can change it) |
| Main concern | Who shares it and how to update it | When to refetch and how to cache |
Client state is born in the browser and dies in the browser. The fact that you turned on dark mode doesn’t need to be asked of the server. This state itself is the source of truth.
Server state is different. The post list on your screen isn’t the real thing — it’s a copy of the original on the server. While you were looking at it, another user may have added a post. In other words, server state goes stale if you leave it alone. That brings concerns like “when should I refetch”, “how long should I cache what I fetched”, and “when multiple screens show the same data, how do I refresh them all together.” None of these concerns exist for client state.
useEffect + useState reveals what it really was.Why this distinction changes your tool choice #
Because the two kinds of state have different natures, the tools that fit them differ too.
A tool for server state should provide caching, automatic refetching, loading and error states, and background updates out of the box. The standard in this space is TanStack Query (formerly React Query). We cover it in #2.
A tool for client state should share global values lightly and re-render only the components that actually need them. Here you have several options. We’ll look at Zustand (#3), Jotai (#4), and the venerable Redux Toolkit (#5) in turn.
Let me call out the most common mistake right away: dumping all your server data into a global client state store like Redux or Zustand. This was treated as standard for a while, but doing it means you have to rebuild caching, refetching, and freshness management by hand. Leaving server state to a server-state tool is far simpler.
[Common anti-pattern]
All state → one giant global store (Redux, etc.)
- dark mode goes here
- the post list fetched from the server also goes here
- caching / refetching / loading all implemented by hand
[Recommended structure]
Server state → TanStack Query (caching, refetch, freshness automatic)
Client state → Zustand / Jotai / Context (lightweight global sharing)
Local state → useState / useReducer (unchanged)So when do you still use useState? #
To avoid a misunderstanding, let me be clear: this series is not telling you to abandon useState. State used inside a single component — a form’s input value, one toggle — is still best served by useState. A new tool only becomes necessary when these conditions stack up:
- Several components need to share the same state.
- Those components sit far apart in the tree, making props awkward.
- (For server state) you also have to worry about caching and refetching.
If none of these apply, the built-in tools are enough. Don’t forget that adding a tool is itself a cost.
What you’ll learn in this series #
Each part examines one tool through the same lens. It’s structured so you can consistently compare installation, basic usage, re-render behavior, and which situations it fits.
- #2 TanStack Query — automating caching and refetching of server state
- #3 Zustand — global client state with minimal boilerplate
- #4 Jotai — a bottom-up model that breaks state into small atoms
- #5 Redux Toolkit — the current face of a standard still living in many codebases
- #6 A decision guide — what to pick in which situation
Wrapping up #
The conclusion of this post is one sentence. There is client state and server state, they differ in nature, and so the tools that fit them differ.
- Client state — self-contained in the browser, the source of truth itself
- Server state — a copy whose original lives on the server, goes stale if left alone
Every time you build a new screen, ask yourself: “Is this value self-contained in the browser, or a copy of server data?” This single question decides which tool you’ll reach for in the rest of the series.
In the next post, “React State Management in Depth #2: Server State with TanStack Query,” we start with server state first. You’ll see firsthand how the data fetching you used to write with useEffect + useState shrinks to a few lines.