A small React application can get away with almost anything.
Keep some values in useState, pass a few props down the tree, add Context when prop drilling gets annoying, and move on. For a prototype or a product with a handful of screens, that may be exactly the right level of engineering.
The trouble starts when the product becomes a real product. More screens appear. Several teams touch the same frontend. Data has to move between distant parts of the UI. Some state comes from APIs, some exists only for the current session, and some should disappear as soon as a modal closes. Decisions that once seemed minor start affecting release velocity, debugging, and infrastructure costs.
That is where React architecture consulting can be useful: state decisions make more sense when they are considered as part of the application’s broader design rather than as a standalone library choice. As a React product grows, its application architecture determines how state is organized, shared, and maintained.
The important question is therefore not “Which state management library should we use?” It is “What kind of state do we have, who owns it, and how far does it actually need to travel?”
The first scaling mistake: treating every value as global state
React state management gets messy when an application stops distinguishing between different kinds of state.
A search field’s current text is state. So is whether a dropdown is open. But neither necessarily belongs in a global store.
Now consider the selected organization in a B2B SaaS product. That choice may affect navigation, permissions, dashboards, billing, and API requests. It has a very different scope and lifetime. Putting it in local component state would be awkward; treating it like a temporary UI flag would be worse.
There is also server state: customers, invoices, product catalogs, orders, notifications, and other data whose authoritative version exists somewhere outside the browser.
Those categories should not automatically share the same storage mechanism.
Local UI state generally belongs close to the component that controls it. Shared client state may justify Context or a dedicated store. Remote data needs caching and synchronization rules. Mixing all three into one global object creates a system that becomes harder to reason about with every feature.
A useful test is surprisingly simple: if this component disappeared, should the state disappear with it? If the answer is yes, globalizing it probably needs a good reason.
Your component tree is part of the state strategy
State architecture and component structure are tightly connected.
Suppose a page has a deeply nested checkout flow. A value starts at the page level and gets passed through four components before reaching the button that actually needs it. The intermediate components do not care about the value; they merely forward it.
That is classic prop drilling, and sometimes it is perfectly acceptable. A short chain of props is often clearer than introducing another abstraction.
The problem is the accumulated version. Once dozens of values travel through unrelated components, developers start making architectural decisions based on convenience. A global store appears attractive because anything can read from it. Six months later, nobody is quite sure which components depend on which pieces of state.
Context can remove some of the plumbing. React’s useContext lets components subscribe to a value supplied higher in the tree, but consumers are re-rendered when that context value changes. A broad provider containing frequently changing data can therefore create its own performance problems.
The answer isn’t “never use Context.” Context is a good fit for things such as themes, localization, authentication information, or other cross-cutting dependencies where the update pattern is relatively controlled.
It is a poor substitute for thinking about ownership.
A global store is useful. A global junk drawer isn’t.
Redux has survived several waves of frontend tooling for a reason: large applications sometimes genuinely need centralized, predictable state.
Its value becomes clearer when many parts of an application need to react to the same business events. A complex workflow can have explicit actions, predictable state transitions, selectors, middleware, and debugging tools. Redux DevTools can also show the history of state changes, which is particularly useful when tracking down a bug that depends on a sequence of user actions.
But centralized state comes with a price.
If every feature is allowed to put whatever it wants into one enormous store, the store becomes an unofficial dependency layer for the entire frontend. Components start knowing too much about unrelated domains. A seemingly local change can affect code several features away.
Modern Redux has moved considerably away from the old reputation for boilerplate. The Redux maintainers explicitly recommend Redux Toolkit for new Redux applications, rather than writing Redux logic against the core APIs by hand. Redux Toolkit provides conventions and utilities for slices, store configuration, immutable updates, async logic, and other common patterns.
That still doesn’t make Redux automatically appropriate.
For a small application with a few shared values, introducing a centralized store can be more machinery than the problem deserves. For a large product with complex cross-feature workflows, refusing to centralize anything can be just as costly.
Scale changes the tradeoff.
Server data should have a lifecycle of its own
One of the most common architectural tangles happens when API data is treated as if it were ordinary client state.
Imagine an admin dashboard that fetches customer records. The data loads, gets copied into a Redux slice, and then another component modifies its local copy. A mutation succeeds, but the original list is now stale. More code is added to refresh it. Then another screen has its own copy.
The application has gradually built a synchronization system without intending to.
Server state has different requirements. It can become stale independently of what the UI is doing. Requests can fail or overlap. A mutation may invalidate several related queries. The application may need caching, refetching, optimistic updates, or background synchronization.
That is why libraries and patterns designed specifically around server state can be a better fit than forcing everything through a general-purpose client store.
Redux itself reflects this distinction: its official tutorials recommend RTK Query for fetching and caching data in modern Redux applications.
The architectural principle matters more than the specific tool. Keep the source of truth clear.
Performance problems usually start with subscriptions, not the word “global”
It is easy to make state management sound like a performance problem by default. It isn’t.
A global store does not inherently make a React application slow. Neither does Context. Performance depends on what subscribes to what, how frequently values change, how much work happens during rendering, and whether expensive computations are repeated unnecessarily.
Consider a live dashboard updating several times per second. If a rapidly changing value sits inside a context consumed by a large section of the interface, that update can affect far more components than necessary. A narrowly scoped subscription may be a better design.

