Blogged Answers: Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux)

This is a post in the Blogged Answers series.


Definitive answers and clarification on the purpose and use cases for Context and Redux

Introduction

"Context vs Redux" has been one of the most widely debated topics within the React community ever since the current React Context API was released. Sadly, most of this "debate" stems from confusion over the purpose and use cases for these two tools. I've answered various questions about Context and Redux hundreds of times across the internet (including my posts Redux - Not Dead Yet!, React, Redux, and Context Behavior, A (Mostly) Complete Guide to React Rendering Behavior, and When (and when not) to Reach for Redux), yet the confusion continues to get worse.

Given the prevalence of questions on this topic, I'm putting together this post as a definitive answer to those questions. I'll try to clarify what Context and Redux actually are, how they're meant to be used, how they're different, and when you should use them.

TL;DR

Are Context and Redux the same thing?

No. They are different tools that do different things, and you use them for different purposes.

Is Context a "state management" tool?

No. Context is a form of Dependency Injection. It is a transport mechanism - it doesn't "manage" anything. Any "state management" is done by you and your own code, typically via useState/useReducer.

Are Context and useReducer a replacement for Redux?

No. They have some similarities and overlap, but there are major differences in their capabilities.

When should I use Context?

Any time you have some value that you want to make accessible to a portion of your React component tree, without passing that value down as props through each level of components.

When should I use Context and useReducer?

When you have moderately complex React component state management needs within a specific section of your application.

When should I use Redux instead?

Redux is most useful in cases when:

  • You have larger amounts of application state that are needed in many places in the app
  • The app state is updated frequently over time
  • The logic to update that state may be complex
  • The app has a medium or large-sized codebase, and might be worked on by many people
  • You want to be able to understand when, why, and how the state in your application has updated, and visualize the changes to your state over time
  • You need more powerful capabilities for managing side effects, persistence, and data serialization

Table of Contents

Understanding Context and Redux

In order to use any tool correctly, it's critical to understand:

  • What its purpose is
  • What problems it's trying to solve
  • When and why it was originally created

It's also critical to understand what problems you are trying to solve in your own application right now, and pick the tools that solve your problem the best - not because someone else said you should use them, not because they’re popular, but because this is what works best for you in this particular situation.

Most of the confusion over "Context vs Redux" stems from a lack of understanding about what these tools actually do, and what problems they solve. So, in order to actually know when to use them, we need to first clearly define what they do and what problems they solve.

What is React Context?

Let's start by looking at the actual description of Context from the React docs:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

In a typical React application, data is passed top-down (parent to child) via props, but this can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.

Notice that it does not say anything about "managing" values - it only refers to "passing" and "sharing" values.

The current React Context API (React.createContext()) was first released in React 16.3. It replaced the legacy context API, which had been available since early versions of React, but had major design flaws. The primary problem with legacy context was that updates to values passed down via context could be "blocked" if a component skipped rendering via shouldComponentUpdate. Since many components relied on shouldComponentUpdate for performance optimizations, that made legacy context useless for passing down plain data. createContext() was designed to solve that problem, so that any update to a value will be seen in child components even if a component in the middle skips rendering.

Using Context

Using React Context in an app requires a few steps:

  • First, call const MyContext = React.createContext() to create a context object instance
  • In a parent component, render <MyContext.Provider value={someValue}>. This puts some single piece of data into the context. That value could be anything - a string, a number, an object, an array, a class instance, an event emitter, and so on.
  • Then, in any component nested inside that provider, call const theContextValue = useContext(MyContext).

Whenever the parent component re-renders and passes in a new reference to the context provider as the value, any component that reads from that context will be forced to re-render.

Most commonly, the value for a context is something that comes from React component state, along these lines:

function ParentComponent() {
  const [counter, setCounter] = useState(0);

  // Create an object containing both the value and the setter
  const contextValue = {counter, setCounter};

  return (
    <MyContext.Provider value={contextValue}>
      <SomeChildComponent />
    </MyContext.Provider>
  )
}

A child component then can call useContext and read the value:

function NestedChildComponent() {
  const { counter, setCounter } = useContext(MyContext);

  // do something with the counter value and setter
}

Purpose and Use Cases for Context

Based on that, we can see that Context doesn't actually "manage" anything at all. Instead, it's like a pipe or a wormhole. You put something in the top end of the pipe using the <MyContext.Provider>, and that one thing (whatever it is) goes down through the pipe until it pops out the other end where another component asks for it with useContext(MyProvider).

So, the primary purpose for using Context is to avoid "prop-drilling". Rather than pass this value down as a prop, explicitly, through every level of the component tree that needs it, any component that's nested inside the <MyContext.Provider> can just say useContext(MyContext) to grab the value as needed. This does simplify the code, because we don't have to write all the extra prop-passing logic.

Conceptually, this is a form of "Dependency Injection". We know that the child component needs a value of a certain type, but it doesn't try to create or set up that value itself. Instead, it assumes that some parent component will pass down that value, at runtime.

What is Redux?

For comparison, let's look at the description from the "Redux Essentials" tutorial in the Redux docs:

Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.

Redux helps you manage "global" state - state that is needed across many parts of your application.

The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.

Note that this description:

  • specifically refers to "managing state"
  • says that the purpose of Redux is to help you understand how state changes over time

Historically, Redux was originally created as an implementation of the "Flux Architecture", which was a pattern first suggested by Facebook in 2014, a year after React came out. Following that announcement, the community created dozens of Flux-inspired libraries with varying approaches to the Flux concepts. Redux came out in 2015, and quickly won the "Flux Wars" because it had the best design, matched the problems people were trying to solve, and worked great with React.

Architecturally, Redux emphasizes using functional programming principles to help you write as much of your code as possible as predictable "reducer" functions, and separating the idea of "what event happened" from the logic that determines "how the state updates when that event happens". Redux also uses middleware as a way to extend the capabilities of the Redux store, including handling side effects.

Redux also has the Redux Devtools, which allow you to see the history of actions and state changes in your app over time.

Redux and React

Redux itself is UI-agnostic - you can use it with any UI layer (React, Vue, Angular, vanilla JS, etc), or without any UI at all.

That said, Redux is most commonly used with React. The React-Redux library is the official UI binding layer that lets React components interact with a Redux store by reading values from Redux state and dispatching actions. So, when most people refer to "Redux", they actually mean "using a Redux store and the React-Redux library together".

React-Redux allows any React component in the application to talk to the Redux store. This is only possible because React-Redux uses Context internally. However, it's critical to note that React-Redux only passes down the Redux store instance via context, not the current state value!. This is actually an example of using Context for dependency injection, as mentioned above. We know that our Redux-connected React components need to talk to a Redux store, but we don't know or care which Redux store that is when we define the component. The actual Redux store is injected into the tree at runtime using the React-Redux <Provider> component.

Because of this, React-Redux can also be used to avoid prop-drilling, specifically because React-Redux uses Context internally. Instead of explicitly putting a new value into a <MyContext.Provider> yourself, you can put that data into the Redux store and then access it anywhere.

Purposes and Use Cases for (React-)Redux

The primary reason to use Redux is captured in the description from the Redux docs:

The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.

There are additional reasons why you might want to use Redux. "Avoiding prop-drilling" is one of those other reasons. Many people chose Redux early on specifically to let them avoid prop-drilling, because React's legacy context was broken and React-Redux worked correctly.

Other valid reasons to use Redux include:

  • Wanting to write your state management logic completely separate from the UI layer
  • Sharing state management logic between different UI layers (such as an application that is being migrated from AngularJS to React)
  • Using the power of Redux middleware to add additional logic when actions are dispatched
  • Being able to persist portions of the Redux state
  • Enabling bug reports that can be replayed by developers
  • Faster debugging of logic and UI while in development

Dan Abramov listed a number of these use cases when he wrote his post You Might Not Need Redux, all the way back in 2016.

Why Context is Not "State Management"

"State" is any data that describes the behavior of an application. We could divide that into categories like "server state", "communications state", and "location state" if we want to, but the key point is that there is data being stored, read, updated, and used.

David Khourshid, author of the XState library and an expert on state machines, said:

"State management is how state changes over time."

Based on that, we can say that "state management" means having ways to:

  • store an initial value
  • read the current value
  • update a value

There's also typically a way to be notified when the current value has changed.

React's useState and useReducer hooks are good example of state management. With both of those hooks, you can:

  • store an initial value by calling the hook
  • read the current value, also by calling the hook
  • update the value by calling the supplied setState or dispatch function
  • Know that the value has been updated because the component re-rendered

Similarly, Redux and MobX are clearly state management as well:

  • Redux stores an initial value by calling the root reducer, lets you read the current value with store.getState(), updates the value with store.dispatch(action), and notifies listeners that the store updated via store.subscribe(listener)
  • MobX stores an initial value by assigning field values in a store class, lets you read the current value by accessing the store's fields, updates values by assigning to those fields, and notifies that changes happened via autorun() and computed()

We can even say that server caching tools like React-Query, SWR, Apollo, and Urql fit the definition of "state management" - they store initial values based on the fetched data, return the current value via their hooks, allow updates via "server mutations", and notify of changes via re-rendering the component.

React Context does not meet those criteria. Therefore, Context is not a "state management" tool!

As we established earlier, Context does not "store" anything itself. The parent component that renders a <MyContext.Provider> is responsible for deciding what value is passed into the context, and that value typically is based on React component state. The actual "state management" is happening with the useState/useReducer hook.

As David Khourshid also said:

Context is how state (that exists somewhere already) is shared with other components.

Context has little to do with state management.

Or, as a recent tweet put it:

I guess Context is more like hidden props than abstracted state.

Think of it this way. We could have written the exact same useState/useReducer code, but prop-drilled the data and the update function down through the component tree. The actual behavior of the app would have been the same overall. All Context does for us is let us skip the prop-drilling.

Comparing Context and Redux

Let's review what capabilities Context and React+Redux actually have:

  • Context
    • Does not store or "manage" anything
    • Only works in React components
    • Passes down a single value, which could be anything (primitive, objects, classes, etc)
    • Allows reading that single value
    • Can be used to avoid prop-drilling
    • Does show the current context value for both Provider and Consumer components in the React DevTools, but does not show any history of how that value changed over time
    • Updates consuming components when the context value changes, but with no way to skip updates
    • Does not include any mechanism for side effects - it's purely for rendering components
  • React+Redux
    • Stores and manages a single value (which is typically an object)
    • Works with any UI, including outside of React components
    • Allows reading that single value
    • Can be used to avoid prop-drilling
    • Can update the value via dispatching an action and running reducers
    • Has DevTools that show the history of all dispatched actions and state changes over time
    • Uses middleware to allow app code to trigger side effects
    • Allows components to subscribe to store updates, extract specific pieces of the store state, and only re-render when those values change

So, clearly these are very different tools with different capabilities. The only overlap between them, really, is "can be used to avoid prop-drilling".

Context and useReducer

One problem with the "Context vs Redux" discussions is that people often actually mean "I'm using useReducer to manage my state, and Context to pass down that value". But, they never state that explicitly - they just say "I'm using Context". That's a common cause of the confusion I see, and it's really unfortunate because it helps perpetuate the idea that Context "manages state".

So, let's talk about the Context + useReducer combination specifically. Yes, Context + useReducer does look an awful lot like Redux + React-Redux. They both have:

  • A stored value
  • A reducer function
  • dispatching of actions
  • a way to pass down that value and read it in nested components

However, there's still a number of very significant differences in the capabilities and behaviors of Context + useReducer vs those of Redux + React-Redux. I covered the key points in my posts React, Redux, and Context Behavior and A (Mostly) Complete Guide to React Rendering Behavior. Summarizing here:

  • Context + useReducer relies on passing the current state value via Context. React-Redux passes the current Redux store instance via Context.
  • That means that when useReducer produces a new state value, all components that are subscribed to that context will be forced to re-render, even if they only care about part of the data. This may lead to performances issues, depending on the size of the state value, how many components are subscribed to that data, and how often they re-render. With React-Redux, components can subscribe to specific pieces of the store state, and only re-render when those values change.

In addition, there's some other important differences as well:

  • Context + useReducer are React features, and therefore cannot be used outside of React. A Redux store is independent of any UI, and so it can be used separate from React.
  • The React DevTools allow viewing the current context value, but not any of the historical values or changes over time. The Redux DevTools allow seeing all actions that were dispatched, the contents of each action, the state as it existed after each action was processed, and the diffs between each state over time.
  • useReducer does not have middleware. You can do some side-effect-y things with useEffect in combination with useReducer, and I've even seen some attempts to wrap useReducer with something that resembles a middleware, but both of those are severely limited in comparison to the functionality and capabilities of Redux middleware.

It's worth repeating what Sebastian Markbage (React core team architect) said about the uses for Context:

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

There's a lot of posts out there that recommend setting up multiple separate contexts for different chunks of state, both to cut down on unnecessary re-renders and to scope concerns. Some of those also suggest adding your own "context selector components", which require a mixture of React.memo(), useMemo(), and carefully splitting things up so there's two separate contexts for each segment of state (one for the data, and one for the updater functions). Sure, it's possible to write code that way, but at that point you're just reinventing React-Redux, poorly.

So, even though Context + useReducer sorta-resemble Redux + React-Redux at a quick glance... they are not fully equivalent and cannot truly replace Redux!

Choosing the Right Tool

As I said earlier, it's critical to understand what problems a tool solves, and know what problems you have, in order to correctly choose the right tool to solve your problems.

Use Case Summary

Let's recap the use cases for each of these:

  • Context:
    • Passing down a value to nested components without prop-drilling
  • useReducer
    • Moderately complex React component state management using a reducer function
  • Context + useReducer:
    • Moderately complex React component state management using a reducer function, and passing that state value down to nested components without prop-drilling
  • Redux
    • Moderate to highly complex state management using reducer functions
    • Traceability for when, why, and how state changed over time
    • Wanting to write your state management logic completely separate from the UI layer
    • Sharing state management logic between different UI layers
    • Using the power of Redux middleware to add additional logic when actions are dispatched
    • Being able to persist portions of the Redux state
    • Enabling bug reports that can be replayed by developers
    • Faster debugging of logic and UI while in development
  • Redux + React-Redux
    • All of the use cases for Redux, plus interacting with the Redux store in your React components

Again, these are different tools that solve different problems!

Recommendations

So, how do you decide whether to use Context, Context + useReducer, or Redux + React-Redux?

You need to determine which of these tools best matches the set of problems that you're trying to solve!

  • If the only thing you need to do is avoid prop-drilling, then use Context
  • If you've got some moderately complex React component state, or just really don't want to use an external library, go with Context + useReducer
  • If you want better traceability of the changes to your state over time, need to ensure that only specific components re-render when the state changes, need more powerful capabilities for managing side effects, or have other similar problems, use Redux + React-Redux

My personal opinion is that if you get past 2-3 state-related contexts in an application, you're re-inventing a weaker version of React-Redux and should just switch to using Redux.

Another common concern is that "using Redux means too much 'boilerplate'". Those complaints are very outdated, as "modern Redux" is significantly easier to learn and use than what you may have seen before. Our official Redux Toolkit package eliminates those "boilerplate" concerns, and the React-Redux hooks API simplifies using Redux in your React components. As one user recently told me:

We just switched from context and hooks over to RTK on one of our production application's frontends. That thing processes a little over $1B/year. Fantastic stuff in the toolkit. The RTK is the polish that helped me convince the rest of the teams to buy into the refactor. I also did a boilerplate analysis for that refactor and it's actually LESS boilerplate to use the RTK than it is to use the recommended dispatch pattern in contexts. Not to mention how much easier it is to process data.

Yes, adding RTK and React-Redux as dependencies does add additional byte size to your application bundle over just Context + useReducer, because those are built in to React. But, the tradeoffs are worth it - better state traceability, simpler and more predictable logic, and improved component rendering performance.

It's also important to point out that these are not mutually exclusive options - you can use Redux, Context, and useReducer together at the same time! We specifically encourage putting "global state" in Redux and "local state" in React components, and carefully deciding whether each piece of state should live in Redux or component state. So, you can use Redux for some state that's global, and useReducer + Context for some state that's more local, and Context by itself for some semi-static values, all at the same time in the same application.

To be clear, I'm not saying that all apps should use Redux, or that Redux is always a better choice! There's many nuances to this discussion. I am saying that Redux is a valid choice, there are many reasons to choose Redux, and the tradeoffs for choosing Redux are a net win more often than many people think.

And finally, Context and Redux are not the only tools to think about. There's many other tools out there that solve other aspects of state management in different ways. MobX is another widely used option that uses OOP and observables to automatically update data dependencies. Jotai, Recoil, and Zustand offer lighter-weight state update approaches. Data fetching libraries like React Query, SWR, Apollo, and Urql all provide abstractions that simplify common patterns for working with cached server state (and the upcoming "RTK Query" library will do the same for Redux Toolkit). Again, these are different tools, with different purposes and use cases, and are worth evaluating based on your use case.

Final Thoughts

I realize that this post won't stop the seemingly never-ending debate over "Context vs Redux?!?!?!?!?". There's too many people out there, too many conflicting ideas, and too much miscommunication and misinformation.

Having said that, I hope that this post has clarified what these tools actually do, how they're different, and when you should actually consider using them. (And maybe, just maybe, some folks will read this article and not feel the need to post the same question that's been asked a million times already...)

Further Information


This is a post in the Blogged Answers series. Other posts in this series: