Context fallacy: why useContext is not a state manager
When developing a React application, we typically focus on a few core principles: how to retrieve data, how to store it, how to mutate it, and how to sync it with the backend. While the entire frontend can be simplified to these basics, the reality is much more complex. We often run into issues keeping data consistent across deeply nested component trees.
Modern applications are no longer just simple forms handled by a few local states. We are building sophisticated interfaces where the state of the entire page is interconnected. Changing a title in a settings panel should reflect in the sidebar, completing a task should update a status badge in the header. As a result, we rely on state managers—which, in 2026, are far more diverse and specialized than the Redux-dominated landscape of 2019.
One common solution is React Context. It’s built-in, easy to use, and works well for passing data through the component tree without prop drilling. Because of that, it’s often used as a lightweight state management solution.
And to be fair—this works. Up to a point.
In this article, I won’t focus on general best practices or debate whether you need a state manager at all. Instead, I want to highlight why useContext should not be used as a state manager. It was never intended for that purpose. It was designed as a transportation layer—a “Global Constant Store” for your application. It should hold data that rarely changes or only updates during a full application re-initialization (like authentication or localization).
The “read-only” approach
Most sophisticated applications require an initialization phase—fetching configurations or validating tokens before rendering the main content. Storing a user profile, theme, or global config in Context is a sound approach.
Why? Because we rely on the fact that these values are set during init and remain essentially read-only. In many cases, you don’t even need to provide a setter function in the Context provider because the primary goal is simply to consume the data.
// ✅ The "Read-Only" Sweet Spot: Stable Global Dataconst AppConfigContext = createContext();
export const AppConfigProvider = ({ config, user, children }) => { // We pass 'config' and 'user' directly. // No useState or setters here because this data is "Set and Forget". return ( <AppConfigContext.Provider value={{ config, user }}> {children} </AppConfigContext.Provider> );};
const Sidebar = () => { // Safe to use! This only renders once because the Provider // value reference never changes after the app init. const { user } = useContext(AppConfigContext); return <nav>Welcome, {user.name}</nav>;};The Breakdown of Interactivity
Before going further, it’s worth reinforcing an important distinction. As mentioned earlier, Context works well for values that change rarely—such as user data, themes, or configuration loaded during initialization.
The problems described below don’t come from Context itself, but from using it to manage frequently changing, highly interactive state.
The trouble begins when useContext is forced to manage highly interactive states. When we add setters for frequently changing values, we run into a core architectural limitation. For optimization, especially in high-performance applications, we rely on memoization (React.memo, useMemo).
The official React documentation explicitly states (source):
“React automatically re-renders all the children that use a particular context starting from the provider that receives a different
value. The previous and the next values are compared with theObject.iscomparison. Skipping re-renders withmemodoes not prevent the children receiving fresh context values.”
That last sentence is the key: Skipping re-renders with memo does not prevent the children receiving fresh context values
This happens because Context lacks Selectors. If you put an object with multiple properties into Context and update even a single property, React sees a new object reference. Since useContext compares the previous and next value with Object.is, any new object—regardless of whether its contents changed—triggers a re-render for all consuming components.
It’s like a broadcast—every consumer updates, even if it doesn’t need to.
Splitting state into multiple contexts can reduce the impact—but it doesn’t solve the core issue. You’re still broadcasting updates, just across smaller channels.
// ❌ The Context Problem: Every consumer re-renders on ANY changeconst DashboardProvider = ({ children }) => { const [state, setState] = useState({ user: {}, theme: 'dark', notifications: [] });
// Even if only 'theme' changes, components using 'user' will re-render return ( <DashboardContext.Provider value={{ state, setState }}> {children} </DashboardContext.Provider> )}In data-heavy UIs—like dashboards or real-time charts—this broadcast behavior quickly becomes a performance bottleneck. Context updates go through all components, even if you use React.memo, making those optimizations ineffective.
⚠️ Important: This doesn’t mean Context should never be used for state. It works fine for simple or infrequently changing values. The issues described here mainly affect applications with frequent updates and complex UI trees.
How Modern State Managers Solve This
So, why does a dedicated state manager (or even strategic prop drilling) perform better?
Imagine a large state object. In your components, you usually only need a small slice of that data. In a modern state manager like Zustand, the library handles the “heavy lifting” via selectors. It checks if the specific properties you’ve selected have actually changed. If a different part of the global state updates, your component stays perfectly still.
// ✅ The Modern Solution: Surgical re-renders via Selectorsimport { create } from 'zustand'
const useStore = create((set) => ({ user: { id: 1, name: 'John' }, theme: 'dark', updateTheme: (newTheme) => set({ theme: newTheme }),}))
function ThemeComponent() { // This component ONLY re-renders if 'theme' changes const theme = useStore((state) => state.theme) return <div>Current theme: {theme}</div>}This “surgical” precision allows an application to remain responsive as the state grows in complexity. You aren’t forcing the browser to perform reconciliation for changes that don’t result in a visible UI update.
Conclusion
As applications grow, maintaining performance becomes a top-tier priority. Unnecessary re-renders are not just a minor lag — they are wasted work that React cannot optimize away.
The primary strength of React is its ability to update only the necessary parts of the UI. If a component’s props haven’t changed, it shouldn’t be touched. It’s that simple.
useContext is an excellent tool for sharing stable, global data across your application. But once your state becomes dynamic, what matters is not just access—but control over updates.
Context doesn’t manage state—it broadcasts it. And broadcasts don’t scale.
← Back to blog