React state management in 2026: choosing between five real options
Most 'state management' debates are category errors. Server data and UI state are different problems and need different tools.
The dominant cost in React data fetching is the request waterfall — each request waiting for a parent to render before it starts. Fetch independent data in parallel with Promise.all on the server or prefetching on the client, colocate requests with the components that need them, and let a cache layer deduplicate rather than lifting every fetch to the top.
A waterfall is a chain of requests where each can only start after the previous one finishes, usually because a component must render before its child's fetch begins.
Component-level fetching creates waterfalls naturally: the page fetches the user, renders, the sidebar then fetches permissions, renders, the widget then fetches data. Each step adds a full round trip even though nothing after the first depends on the previous result.
// Waterfall: 3 sequential round trips
const user = await getUser(id);
const orders = await getOrders(id);
const invoices = await getInvoices(id);
// Parallel: 1 round trip's worth of latency
const [user, orders, invoices] = await Promise.all([
getUser(id),
getOrders(id),
getInvoices(id),
]);| Situation | Fetch where | Why |
|---|---|---|
| Initial page content | Server | No client round trip, no cold cache, indexable |
| Data behind a click | Client | Not needed for first paint |
| Frequently refetched data | Client | Cache, focus refetch, polling |
| Secrets or privileged queries | Server | Credentials never reach the browser |
| Real-time updates | Client | Sockets, subscriptions |
A common effective split: server-render the initial data into the page, then hand it to a client cache as initial state so subsequent interactions are instant and refetching is handled.
Cache keys should describe the request exactly — resource, identifier and every parameter that changes the response. A key of ['orders'] when the query filters by status will serve one filter's results for another's.
A mutation should do three things: apply the change, invalidate what it affected, and handle failure visibly. Optimistic updates add a fourth — rollback — and should only be used where success is overwhelmingly likely and the change is cheap to reverse.
No — colocation is good for maintainability. The anti-pattern is sequential dependency between those fetches. Colocate, but start independent requests in parallel higher up or via prefetching.
Not for read-only pages. You still benefit from it for client-driven interactions: polling, infinite scroll, optimistic mutations and background refetching.
Keep the page in the URL so it is shareable, prefetch the next page on idle, and use cursor-based pagination on the API for stable results with changing data.
Match it to how fast the data actually changes. Reference data can be minutes; a live order queue might be seconds. Defaulting everything to zero causes needless refetch storms.
Harshal Patel
Founder & Lead Engineer, ROVQIX
Harshal leads engineering at ROVQIX, where he has shipped production Next.js, Node.js and PostgreSQL systems for startups, SaaS teams and ecommerce brands. He writes about the trade-offs behind architecture decisions rather than the framework of the week.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Most 'state management' debates are category errors. Server data and UI state are different problems and need different tools.
Most 'Next.js is showing stale data' bugs are one of four caches doing exactly what it was told. Here is the mental model that makes them predictable.
An API is a product with developers as users. Most of what makes one good is consistency, not cleverness.
No spam. Just the occasional case study and craft breakdown.