DEV Community

Cover image for You don't need a state management library in 2026
Info Inlet
Info Inlet

Posted on

You don't need a state management library in 2026

Hot take: if you're reaching for Redux, Zustand, Jotai, or MobX on a fresh project in 2026, there's a good chance you're solving a problem you don't have.

I'm not saying these libraries are bad. I'm saying the ground shifted under them. React 19, the router-level data layer, and the "server state vs. UI state" distinction quietly absorbed 90% of what we used to install a library for.

Let me show you.

First, the lie we tell ourselves

We say "the app needs global state." What we usually mean is:

  1. Server state — data that lives in a database and we're caching on the client (user profile, product list, the current cart from an API).
  2. UI state — genuinely client-only stuff (is the modal open, which tab is active, form draft text).
  3. URL state — filters, search queries, the current page, the selected item.

Here's the trick almost nobody internalizes: most of your "global state" is server state. And a state management library is the worst tool for server state, because it has no idea about caching, revalidation, deduplication, or staleness. You end up hand-rolling all of that in reducers. That's the 400 lines of Redux boilerplate everyone complains about — it's a caching layer wearing a state-management costume.

Split those three buckets and the "I need a global store" feeling mostly evaporates.

Bucket 1: Server state → a query library, not a state library

If you take one thing from this post: server state belongs in TanStack Query (or RTK Query, or your router's loader).

// This is your "global state" for 90% of real apps.
function useCart() {
  return useQuery({
    queryKey: ['cart'],
    queryFn: () => fetch('/api/cart').then(r => r.json()),
  })
}
Enter fullscreen mode Exit fullscreen mode

That's it. Any component, anywhere in the tree, calls useCart(). It's cached, deduplicated, shared, and refetched on focus. No provider you wrote, no reducer, no action creators, no dispatch. Mutations invalidate the key and every subscriber updates:

const { mutate: addItem } = useMutation({
  mutationFn: (id: string) => fetch('/api/cart', { method: 'POST', body: id }),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cart'] }),
})
Enter fullscreen mode Exit fullscreen mode

People install Redux to share the cart across the app. This is that — with cache invalidation you'd otherwise write by hand.

Bucket 2: UI state → React already ships the tools

Modal open? Active tab? useState. Complex local transitions? useReducer. Need to share a little UI state down a subtree without prop-drilling? Context — and yes, plain Context is fine here, because UI state doesn't churn 60 times a second like people fear.

const ThemeContext = createContext<'light' | 'dark'>('light')

function App() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')
  return (
    <ThemeContext value={theme}>   {/* React 19: no more .Provider */}
      <Toolbar onToggle={() => setTheme(t => (t === 'light' ? 'dark' : 'light'))} />
    </ThemeContext>
  )
}
Enter fullscreen mode Exit fullscreen mode

The classic objection: "Context re-renders the whole subtree!" True — for high-frequency state (cursor position, live-dragging). But that's a narrow case, and the fix is to split contexts or colocate state, not to install a 12KB store for your dark-mode toggle.

Bucket 3: URL state → the most underused store in your app

Filters, tabs, the selected row, pagination — put them in the URL. It's shareable, back-button-friendly, survives refresh, and needs zero libraries.

const [params, setParams] = useSearchParams()
const tab = params.get('tab') ?? 'overview'

<button onClick={() => setParams({ tab: 'billing' })}>Billing</button>
Enter fullscreen mode Exit fullscreen mode

Every "which panel is open" that you were about to lift into a global store is probably a ?tab= away. Bonus: your users can bookmark it.

What React 19 changed specifically

This is the part that pushed me over the line. React 19's primitives cover cases that used to justify a library:

  • useOptimistic — optimistic UI (the #1 reason people reached for a client store to "hold the pending value") is now a built-in hook.
  • useActionState + Actions — form-and-mutation state, pending flags, and errors without a single useState triplet.
  • use() — read a promise or context conditionally; suspense-friendly data reads without a wrapper store.
  • Server Components / Server Actions — a whole category of state simply never reaches the client. You can't have a client-state problem for data that stays on the server.
function LikeButton({ postId, initialLikes }) {
  const [likes, addLike] = useOptimistic(initialLikes, (n) => n + 1)
  return (
    <form action={async () => { addLike(); await like(postId) }}>
      <button>{likes}</button>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Five years ago this snippet was a Redux slice, an async thunk, and an optimistic-update middleware. Now it's eight lines.

Okay — when SHOULD you still use one?

I'm not a zealot. Two cases still earn a library:

  1. Genuinely complex, high-frequency, cross-tree client state. Think a Figma-style canvas, a spreadsheet engine, a collaborative editor, a DAW. Many subscribers, many updates per second, fine-grained reactivity mandatory. Here Zustand or Jotai (atomic, surgical re-renders) genuinely earn their keep — Context would melt.

  2. A large existing team/app with established patterns. Consistency has value. If your 40-person codebase is all Redux Toolkit and it works, "you technically don't need it" is not a refactor ticket. Don't rewrite working software to win an internet argument.

Notice both are about scale and update frequency, not "my app has more than one page." That's the line.

The 2026 default

Start with nothing. Then add, in this order, only when you feel the pain:

  1. TanStack Query the moment you touch a server. (This is not optional — it's the actual win.)
  2. URL state for anything shareable or navigational.
  3. useState / useReducer / Context for local and small-shared UI state.
  4. Zustand / Jotai only when you hit real high-frequency, cross-tree client state.

Nine out of ten apps never reach step 4. That's the whole point.


So here's the question for the comments: what's the last app you built where a global state library was genuinely load-bearing — and be honest, was it client state, or were you just caching the server by hand? 👇

(If this saved you a dependency, a ❤️ and a 🔖 help it reach the next dev who's about to npm install redux out of habit.)

Top comments (1)

Collapse
 
lanba_a7241f798242396b44f profile image
lanba

Solid post, but I'd push back a bit on URL state being a free win.

Works great for filters and tabs. The second you need nested/dependent state — a multi-step wizard, a dashboard with configurable panels — you end up serializing JSON into a query param, writing a Zod parser, and debugging weird shares when someone sends a URL with a stale schema. At that point a Jotai atom with persistence is smaller and saner.

Otherwise agreed — "I need Redux" almost always means "I need a server cache." Took me way too long to internalize that one. 🙃