Skip to content
lazy devs
5 min readLazy Devs

React state management in 2026

A practical map of React state in 2026: server state vs client state, where Redux still fits, and why most apps need far less global state than they think.

Every few months someone declares a winner in React state management, and every few months a new project ships with five different state libraries fighting over the same data. The truth in 2026 is calmer than the discourse: most of your "state" is just server data that needs caching, and the rest is smaller than you think. Let's sort out what actually belongs where.

The split that matters: server state vs client state

The single most useful mental model is also the oldest one that people still skip. Your app has two kinds of state, and they have almost nothing in common.

Server state is data you do not own. It lives in Postgres, it can change without you, it can go stale, and it needs caching, refetching, and invalidation. Think of a list of orders, a user profile, a search result.

Client state is data your UI owns. A modal being open, the current tab, a form draft, a theme toggle. It never needs to be fetched and it disappears when the tab closes.

The classic mistake is dumping server data into a global client store (Redux, Zustand, whatever) and then hand-writing the caching layer yourself. You end up reinventing loading flags, race-condition handling, and stale data invalidation. That is the part you should never write by hand.

For server state, use a query library

In 2026 this is settled. TanStack Query (the library formerly known as React Query) is the default for client-rendered data, and it earns its keep. It handles caching, deduplication, background refetching, and mutation invalidation so you do not have to.

// hooks/useOrders.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
 
type Order = { id: string; total: number; status: "open" | "shipped" };
 
export function useOrders() {
  return useQuery({
    queryKey: ["orders"],
    queryFn: async (): Promise<Order[]> => {
      const res = await fetch("/api/orders");
      if (!res.ok) throw new Error("Failed to load orders");
      return res.json();
    },
    staleTime: 30_000, // treat data as fresh for 30s, no refetch storm
  });
}
 
export function useShipOrder() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (id: string) =>
      fetch(`/api/orders/${id}/ship`, { method: "POST" }),
    // refetch the list once the server confirms
    onSuccess: () => qc.invalidateQueries({ queryKey: ["orders"] }),
  });
}

That invalidateQueries call is the whole point. You changed something on the server, so you tell the cache it is stale and let the library refetch. No manual store updates, no drift between what the UI shows and what the database holds.

Where Next.js changes the picture

If you are on the App Router, a large chunk of your server state never reaches the client at all. You fetch in a Server Component, render the result, and ship HTML. There is no loading spinner and no client cache because the data was resolved before the page existed.

// app/orders/page.tsx (Server Component)
import { db } from "@/lib/db";
 
export default async function OrdersPage() {
  const orders = await db.order.findMany({ orderBy: { createdAt: "desc" } });
  return (
    <ul>
      {orders.map((o) => (
        <li key={o.id}>{o.id} ({o.status})</li>
      ))}
    </ul>
  );
}

The rule of thumb: fetch on the server when the data is needed for the initial render and does not change while the user stares at it. Reach for TanStack Query when the data is interactive (infinite scroll, polling, optimistic updates, anything that mutates and refetches without a full navigation). Plenty of real apps use both, and that is correct, not a smell. Server Components for the first paint, a query library for the lively bits.

Server Actions cover the mutation side. A form posts to an action, the action writes to the database and calls revalidatePath or revalidateTag (the same invalidation machinery covered in Next.js caching), and the relevant Server Components re-render with fresh data. For server-rendered pages this replaces a good amount of what you used to reach into a client store for.

Client state: smaller than the internet thinks

Once server data is handled properly, the actual global client state left in most apps is tiny. A theme. The signed-in user object. Maybe a cart. That is often the entire list.

Start with the primitives. useState for local component state, useReducer when transitions get gnarly, and the Context API for the handful of values that genuinely need to be read from many places (theme, auth, locale). Context gets unfairly bullied for performance, but the problem is almost always putting frequently-changing values in one giant context. Split your contexts by update frequency and the issue mostly evaporates.

When you do reach for a store

If you have client state that is shared, updated often, and read in many unrelated places, a small store beats prop drilling and beats a sprawling context. Zustand is the comfortable default in 2026: a few kilobytes, no provider wrapping, no boilerplate ceremony.

// stores/cart.ts
import { create } from "zustand";
 
type CartState = {
  items: Record<string, number>;
  add: (id: string) => void;
  clear: () => void;
};
 
export const useCart = create<CartState>((set) => ({
  items: {},
  add: (id) =>
    set((s) => ({ items: { ...s.items, [id]: (s.items[id] ?? 0) + 1 } })),
  clear: () => set({ items: {} }),
}));

Components subscribe to the exact slice they care about (useCart((s) => s.items[id])), so a quantity change does not re-render the whole tree. That selector-based subscription is the feature that makes Zustand feel light.

So where does Redux fit now?

Redux Toolkit is not dead, and the "Redux is dead" takes are tired. It is no longer the default for a fresh app, but it remains a sane choice for large teams that want one strict, predictable, well-traced pattern across a big codebase, especially where the Redux DevTools time-travel debugging and middleware ecosystem pull real weight. If you already run Redux Toolkit and it is working, migrating for fashion is a waste of a sprint. Just make sure you are using RTK Query for your server data rather than thunks plus hand-rolled caching, because that is the part that ages badly.

What you should not do in 2026 is start a greenfield project by writing plain Redux with action-type constants and switch statements. That era is over, and good riddance.

A decision guide you can actually use

  • Data comes from an API and is needed on first paint: fetch in a Server Component.
  • Data comes from an API and is interactive (polling, infinite scroll, optimistic updates): TanStack Query, or RTK Query if you are already in Redux.
  • One component owns it: useState, or useReducer for complex transitions.
  • A few stable values read widely (theme, auth, locale): Context, split by update frequency.
  • Shared client state updated often across the tree (cart, editor UI, filters): Zustand.
  • Large team wanting one strict pattern everywhere: Redux Toolkit with RTK Query.

The signs you are over-engineering: a global store whose main job is caching API responses, contexts that re-render half your app on every keystroke, or three libraries holding overlapping copies of the same user object. Each of those is a refactor waiting to pay for itself.

The takeaway

Sort your state into server and client before you pick a single tool. Hand the server half to a query library or to Server Components and let them own the caching. Keep the client half small, local by default, and only promote it to a store when sharing genuinely demands it. Do that and React state management in 2026 stops being a debate and goes back to being plumbing, which is exactly where it belongs.

If you want a second pair of eyes on a state layer that has grown teeth, that is the kind of thing our React development team untangles for a living.

Related service

React Development Services

Maintainable React interfaces and applications.

Learn more

Want this built right?

This is the work we do every day. Tell us what you are building and we will show you exactly how we would ship it.

hello@lazydevsagency.com