React State Management: What Actually Works in Production
Choosing the Right React State Management Approach
React state management becomes difficult when applications grow and different types of state are handled with the same tools. The challenge is not choosing between Redux, Context, Zustand or other libraries. The challenge is understanding what state your application actually needs to manage.
We regularly review React applications where state architecture has become a development bottleneck. The issue is usually not the technology itself, but unclear boundaries between server data, user interface state, URL state and application-level client state.
A practical React state management strategy starts by identifying the role of each type of state and choosing the simplest approach that solves the problem.
The Real Problem Is Understanding What State You Are Managing
React state management decisions become much easier when you first identify the type of state involved. Many applications become unnecessarily complex because the same solution is used for every problem.
We typically separate state into four categories:
- Server state: Data that comes from APIs and needs caching, synchronisation and background updates.
- URL state: Information such as filters, searches and pagination that should be shareable and preserved through navigation.
- UI state: Interface behaviour such as modals, dropdowns, forms and component visibility.
- Client state: Application-specific data that needs to be shared across unrelated components.
The right React state management approach depends on the type of state, not on choosing one library for everything. A well-designed application often uses multiple approaches together, with each tool solving the problem it was designed for.
Server State: Using React Query and the Right Data Patterns
Server state is one of the most common areas where React applications become unnecessarily complex. Data from APIs has different requirements from local interface state because it needs loading states, caching, synchronisation and error handling.
Tools such as React Query (TanStack Query) are designed to manage these challenges by handling data fetching, caching and background updates without requiring teams to manually build these systems themselves.
A good server state approach helps applications avoid common problems such as duplicated requests, stale data, inconsistent loading behaviour and unnecessary state management code.
The right solution depends on the application. Some projects need React Query, while others may require different patterns based on their architecture and data requirements. The goal is creating predictable data flows that are easier for teams to maintain.
URL State: The Most Underused State Container
Filters, search queries, pagination, selected tabs—these should live in the URL. Not in Context. Not in Redux. In the URL.
Why? Because URLs are shareable, bookmarkable, and survive page refreshes. When users share a filtered view or bookmark a search result, it should work.
One marketplace we rebuilt had search filters in component state. Users couldn’t share searches. Browser back/forward broke the filters. The previous team planned to add Redux to “fix” it.
We used the URL. Added a custom hook wrapping Next.js router to sync query params with React state. Every filter, sort option, and pagination cursor lives in the URL. Users can share searches. Back button works. No global state needed.
This is particularly important for marketplace development where search and filtering are core features.
UI State: Context Is Fine, Actually
For UI state—modals, dropdowns, sidebars—Context is completely adequate. You don’t need Redux DevTools to debug whether a modal is open.
The “Context is slow” crowd points to re-render issues. They’re right, but the solution isn’t Redux. It’s splitting contexts and using proper memoisation.
Bad Context usage we see constantly:
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState();
const [theme, setTheme] = useState();
const [notifications, setNotifications] = useState();
const [sidebar, setSidebar] = useState();
// 15 more pieces of state...
return (
<AppContext.Provider value={{ /* everything */ }}>
{children}
</AppContext.Provider>
);
}Every state update re-renders every consumer. Changing the sidebar state re-renders the user profile component.
The fix: split contexts by concern. UserContext, ThemeContext, NotificationContext. Components only subscribe to what they need. We’ve seen this change eliminate 90% of unnecessary re-renders.
Client State: When You Actually Need Redux/Zustand
For complex client state that needs to be accessed across unrelated components, you need a proper state management library. Not Context. Not prop drilling.
Examples: shopping cart state, multi-step form progress, collaborative editing state, complex UI coordination.
Redux vs Zustand comes down to team size and complexity:
Redux makes sense when you have 5+ developers and need strict patterns. The boilerplate is annoying, but it enforces consistency. We’ve seen large teams benefit from Redux’s structure—it’s harder to make a mess when the patterns are rigid.
Redux Toolkit (RTK) dramatically reduces boilerplate. If you’re still writing hand-rolled reducers and action creators, you’re doing it wrong. RTK gives you Redux’s benefits without the pain.
Zustand is our default for smaller teams and simpler state. Less boilerplate, easier to learn, still provides centralised state with DevTools support. One project we inherited had 12,000 lines of Redux code managing shopping cart state. We rewrote it in Zustand in 800 lines with identical functionality.
For SaaS development with small to mid-size teams, Zustand hits the sweet spot between simplicity and power.
The Antipattern: Using One Solution for Everything
The worst React codebases we’ve rescued all made the same mistake: choosing one react state management approach and using it for everything.
One SaaS app we took over used Redux for everything. API data in Redux. Form state in Redux. Modal open/closed in Redux. Theme preference in Redux. URL query params duplicated in Redux “for consistency.”
The Redux store had 247 slices. The codebase had 18,000 lines of Redux boilerplate. Simple features took weeks because developers spent more time managing state than building features.
We refactored to a hybrid approach:
- React Query for server state (deleted 8,000 lines of Redux)
- URL for filters and pagination (deleted 2,000 lines of Redux)
- Context for theme and UI state (deleted 3,000 lines of Redux)
- Zustand for shopping cart and complex client state (kept, but simplified from Redux)
Feature velocity doubled. New developers onboarded in days instead of weeks. Bug reports related to state inconsistencies dropped 80%.
This is the kind of work we do with legacy code modernisation—fixing architectural mistakes that compound over time.
Performance: When State Management Actually Matters
State management becomes a performance bottleneck when you have large lists, frequent updates, or complex derived state.
One project we inherited had a real-time dashboard with 1,000+ data points updating every second. The previous team used Context. Every update re-rendered the entire dashboard. The browser couldn’t keep up—CPU pegged at 100%, animations janky, users complaining.
The fix wasn’t just switching libraries. It was rethinking the architecture:
- Split state into independent stores (Zustand) so components only subscribe to their data
- Used
useSyncExternalStorefor fine-grained subscriptions - Moved calculations to Web Workers
- Added virtual scrolling for the list
CPU usage dropped to 15%. Dashboard stayed responsive even with 2,000 data points.
This kind of performance optimisation requires understanding the entire stack, not just swapping libraries.
“The project manager is a highly skilled developer who can provide knowledgeable advice on individual tasks.”
— Justin Brooks, Founder, Fintech Startup
Beware the “Scalable Architecture” Promise
We see it constantly in proposals: “We’ll build a scalable, future-proof architecture.” It sounds reassuring. It’s also meaningless.
Scalability depends on requirements that change. The right state management for 100 users isn’t the same as for 100,000 users. The right architecture for a simple CRUD app isn’t the same as for real-time collaboration. Anyone who promises scalable architecture without understanding your actual use cases is either inexperienced or selling you something.
We don’t promise future-proof architecture. We promise to build what’s right-sized for your current stage—and we’ll tell you honestly what will need to change as you grow. That’s a sustainable relationship, not a fantasy.
What We Actually Recommend
After inheriting dozens of React projects and seeing every react state management mistake possible, here’s our default stack:
- React Query for server state
- URL for navigation state (filters, search, pagination)
- Context for simple UI state (theme, sidebar, modals)
- Zustand for complex client state (shopping cart, multi-step forms, collaborative features)
- Local state (useState) for component-specific state
This isn’t dogmatic. We’ve used Redux on large projects where strict patterns helped. We’ve used Jotai when atomic state fit the use case. We’ve even kept existing Redux when the team was productive with it.
The key insight: match the tool to the state type. Don’t use one solution for everything.
When to Get Help
If your React app has multiple state management libraries, inconsistent patterns, or developers avoiding state changes because they’re “too complicated,” you have an architectural problem.
We specialise in rescuing projects where state management became a bottleneck. Sometimes the fix is refactoring. Sometimes it’s a targeted rebuild of the state layer. We assess honestly—not every project needs a rescue, and we’ll tell you if you don’t.
We’re also upfront about trade-offs. We won’t promise “clean, maintainable code”—that’s subjective and means different things to different developers. We won’t promise “zero bugs”—all software has bugs. What we will promise: transparency about progress and blockers, honest feedback when something is a bad idea, and involvement in the decisions that affect your product.
If you’re wrestling with state management decisions or inheriting a React codebase with unclear patterns, book a free codebase assessment. We’ve been through this enough times to spot the issues quickly and implement fixes that actually work.