React State Management in Depth #1: Client State vs Server State

6 min read

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 component
  • useReducer — local state with complex transition rules
  • useContext — 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.

AspectClient stateServer state
Where the source of truth livesThe browser (this state is the source)The server database (the screen is a copy)
ExamplesDark mode, modal open, form input, selected tabUser list, posts, order history, search results
Needs syncingNo (self-contained in my browser)Yes (must keep up with the server)
FreshnessAlways current (I’m the one changing it)Goes stale over time (others can change it)
Main concernWho shares it and how to update itWhen 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.

Note
This distinction was popularized by Tanner Linsley, the creator of TanStack Query. The term “server state” can feel awkward at first, but once you look at your code through this lens, the thing you used to laboriously hand-write with 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 vs recommended structure
[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.

X