Why Context is not a "state management" tool





TL; DR



Are Context and Redux the same?


No. They are different tools that do different things and are used for different purposes.



Is the context a tool for "state management"?


No. Context is a form of dependency injection. It is a transport mechanism that controls nothing. Any "state management" is done manually, usually using the useState () / useReducer () hooks.



Are Context and useReducer () a replacement for Redux?


No. They are somewhat similar and partially overlap, but differ greatly in terms of capabilities.



When should you use context?


When you want to make some data available to multiple components, but don't want to pass that data as props at every level of the component tree.



When should you use Context and useReducer ()?


When you need to manage the state of a moderately complex component in a specific part of your application.



When should you use Redux?


Redux is most useful when:



  • Large number of stateful components using the same data
  • App status is updated frequently
  • Complex logic for updating state
  • The application has a medium to large codebase and many people are working on it
  • You want to know when, why and how application state is updated and be able to visualize those changes
  • You need more powerful capabilities for handling side effects, stability (persistence) and data serialization




Understanding Context and Redux



To use the tool correctly, it is critical to understand:



  • What is it for
  • What tasks does it solve
  • When and why was it created


It is also important to understand what problems you are trying to solve in your application and use the tools that best solve them, not because someone told you to use them, and not because they are popular, but because they work best for you in a particular situation.



The confusion around Context and Redux is primarily due to a misunderstanding of what these tools are intended for and what tasks they solve. Therefore, before talking about when they should be used, it is necessary to determine what they are and what problems they solve.



What is context?



Let's start by defining the context from the official documentation :



“The context allows you to pass data through the component tree without having to pass props at intermediate levels.



In a typical React application, data is passed from top to bottom (from parent to child) using props. However, this method can be overkill for some types of props (for example, selected language, interface theme) that need to be passed to many components in an application. The context provides a way to distribute such data among components without having to explicitly pass props at each level of the component tree. "



Please note that this definition does not say a word about "management", only about "transfer" and "distribution".



The current context API (React.createContext ()) was first introduced in React 16.3 as a replacement for the deprecated API available in earlier versions of React, but with several design flaws. One of the main problems was that updates to values ​​passed through the context could be "blocked" if the component skipped rendering through shouldComponentUpdate (). Since many components resorted to shouldComponentUpdate () for optimization purposes, passing data through the context became useless. createContext () was designed to address this issue, so any update to the value will be reflected in the child components, even if the intermediate component skips rendering.



Using context


Using context in an application assumes the following:



  • Call const MyContext = React.createContext () to instantiate the context object
  • In the parent component, render & ltMyContext.Provider value = {someValue}>. This puts some data into context. This data can be anything: string, number, object, array, class instance, event handler, etc.
  • Get the context value in any component inside the provider by calling const theContextValue = useContext (MyContext)


When the parent component is updated and the new reference is passed as the provider value, any component that "consumes" the context will be forced to be updated.



Typically, the context value is the state of the component:



import { createContext } from 'react'

export const MyContext = createContext()

export function ParentComponent({ children }) {
  const [counter, setCounter] = useState(0)

  return (
    <MyContext.Provider value={[counter, setCounter]}>
      {children}
    </MyContext.Provider>
  )
}

      
      





The child component can then call the useContext () hook and read the context value:



import { useContext } from 'react'
import { MyContext } from './MyContext'

export function NestedChildComponent() {
  const [counter, setCounter] = useContext(MyContext)

  // ...
}

      
      







We can see that the context does not really control anything. Instead, it is a kind of pipe. You put data at the beginning (top) of the tunnel using <MyContext.Provider>, then this data is rolled down until the component requests it using useContext (MyContext).



Thus, the main purpose of the context is to prevent prop-drilling. Instead of passing data as props at every level of the component tree, any component nested in <MyContext.Provider> can access it through useContext (MyContext). This eliminates the need to write code to implement the prop passing logic.



Conceptually, this is a form of dependency injection... We know that the child needs data of a certain type, but it does not try to create or set this data on its own. Instead, it relies on some ancestor to pass this data at runtime.



What is Redux?



Here's what the Redux Basics definition says :



“Redux is a design pattern and library for managing and updating application state using events called operations. Redux acts as a centralized repository of application state, following rules to ensure predictable state updates.



Redux allows you to manage "global" state - state that is piped to multiple parts of your application.



The patterns and tools provided by Redux make it easier to determine where, when, why, and how state was updated and how the application responded to that change. ”



Please note that this description indicates:



  • State management
  • The purpose of Redux is to determine why and how a state change occurred


Redux was originally an implementation of the "Flux architecture" , a design pattern developed by Facebook in 2014, one year after React was released. Since the advent of Flux, the community has developed many libraries that implement this concept in different ways. Redux appeared in 2015 and quickly became the winner of this competition thanks to its thoughtful design, solving common problems and excellent compatibility with React.



Architecturally, Redux emphatically uses the principles of functional programming, which allows you to write code in the form of predictable "reducers", and separate the idea of ​​"what happened" from the logic that defines "how the state is updated when this event occurs." Redux also uses middleware as a way to extend the capabilities of the store, including handling side effects .



Redux also provides developer tools to explore the history of operations and state changes over time.



Redux and React


Redux itself is independent of the UI - you can use it with any view layer (React, Vue, Angular, vanilla JS, etc.) or no UI at all.



Most often, however, Redux is used in conjunction with React. The React Redux library is the official UI binding layer that allows React components to interact with the Redux store by retrieving values ​​from Redux state and initiating operations. React-Redux uses context internally. However, it should be noted that React-Redux passes through the context a Redux store instance, not the current state value!This is an example of using context for dependency injection. We know that our Redux-connected components need to interact with the Redux store, but we don't know or don't care what that store is when we define the component. The real Redux store is injected into the tree at runtime using the <Provider> component provided by React-Redux.



Therefore, React-Redux can also be used to prevent "drilling" (due to internal use of context). Instead of explicitly passing the new value through <MyContext.Provider>, we can put this data into the Redux store and then retrieve it in the desired component.



Purpose and Use Cases of (React-) Redux


The main purpose of Redux according to the official documentation:



"The patterns and tools provided by Redux make it easier to understand when, where, why, and how a state change occurred, and how the application reacted to it."



There are several more reasons for using Redux. One of these reasons is to prevent "drilling".



Other use cases:



  • Complete separation of the state management logic and the UI layer
  • Distribution of state management logic between different UI layers (for example, in the process of translating an application from AngularJS to React)
  • Using Redux middleware to add extra logic when initializing operations
  • Ability to save portions of Redux state
  • Ability to receive bug reports that can be reproduced by other developers
  • Ability to quickly debug logic and UI during development


Dan Abramov listed these cases in his 2016 article Why You May Not Need Redux .



Why isn't context a tool for "state management"?



State is any data that describes the behavior of an application . We can categorize state into categories like server state, communication state, and local state if we want, but the key aspect is storing, reading, updating and using data.



David Khourshid, author of the XState library and state management specialist, noted in one of his tweets that:



"State management is about changing state over time."



Thus, we can say that "state management" means the following:



  • Storing the initial value
  • Getting the current value
  • Updating a value


Also, there is usually a way to get notified when the current state value has changed.



The React hooks useState () and useReducer () are great examples of state management. With these hooks we can:



  • Store initial value by calling a hook
  • Get the current value also by calling the hook
  • Update the value by calling setState () or dispatch (), respectively
  • Be aware of state updates by re-rendering the component


Redux and MobX also let you manage state:



  • Redux stores the initial value by calling the root reducer, allows reading the current value with store.getState (), updating the value with store.dispatch (action), and receiving state update notifications via store.subscribe (listener)
  • MobX preserves an initial value by assigning a value to a storage class field, allows the current value to be read and updated through storage fields, and receives state update notifications using the autorun () and computed () methods


State management tools can even include server cache tools such as React-Query, SWR, Apollo and Urql - they store an initial value based on fetched data, return the current value using hooks, and allow updating values ​​via " server mutations ”and notify the changes by re-rendering the component.



React Context doesn't match the named criteria. Therefore it is not a state management tool


As noted earlier, the context itself does not store anything. The parent component, which renders <MyContext.Provider>, is responsible for passing the value, which usually depends on the state of the component. The real "state management" comes from the useState () / useReducer () hooks.



David Khourshid also notes:



“Context is how existing state is shared between components. The context does nothing with the state. "



And in a later tweet ,



"I guess the context is like hidden props that abstract state."



Anything context does is to avoid "drilling."



Comparing Context and Redux



Let's compare the capabilities of the context and React + Redux:



  • Context
    • Stores nothing and manages nothing
    • Only works in React components
    • Passes below a simple (single) value, which can be anything (primitive, object, class, etc.)
    • Lets read this simple meaning
    • Can be used to prevent "drilling"
    • Shows the current value for the Provider and Consumer components in the developer tools, but does not show the history of changes to this value
    • Updates consuming components when the value changes, but does not allow the update to be skipped
    • Provides no mechanism for handling side effects - only responsible for rendering


  • React + Redux
    • Stores and manages a simple value (usually an object)
    • Works with any UI as well as outside of React components
    • Lets read this simple meaning
    • Can be used to prevent "drilling"
    • Can update value by initializing operations and running reducers
    • Developer tools show history of initialization of operations and state changes
    • Provides the ability to use middleware to handle side effects
    • Allows components to subscribe to store updates, retrieve specific portions of store state, and control component re-rendering




Obviously, these are completely different tools with different capabilities. The only intersection point between them is to prevent "drilling".



Context and useReducer ()



One of the problems with the "Context versus Redux" discussion is that people often actually mean, "I use useReducer () to manage state and context to pass value." But instead, they just say, "I'm using context." This, in my opinion, is the main reason for the confusion that contributes to the maintenance of the myth that the context "rules the state."



Consider the combination of Context + useReducer (). Yes, this combination looks very similar to Redux + React-Redux. Both of these combinations have:



  • Stored value
  • Reducer function
  • Ability to initialize operations
  • The ability to pass a value and read it in nested components


However, there are still some important differences between them, manifested in their capabilities and behavior. I've noted these differences in React, Redux, and Context Behavior and The (Almost) Complete Guide to Rendering in React . In summary, the following can be noted:



  • Context + useReducer () relies on passing the current value through the context. React-Redux passes the current Redux store instance through the context
  • This means that when useReducer () produces a new value, all components subscribed to the context are forced to redraw, even if they only use some of the data. This can lead to performance issues depending on the size of the state value, the number of signed components, and the frequency of re-rendering. With React-Redux, components can subscribe to a specific part of the store value and only redraw when that part changes


There are other important differences:



  • Context + useReducer () are built-in capabilities of React and cannot be used outside of it. The Redux store is UI independent, so it can be used separately from React
  • React DevTools , . Redux DevTools , ( , type and payload),
  • useReducer() middleware. useEffect() useReducer(), useReducer() middleware, Redux middleware


Here's what Sebastian Markbage (React Core Team Architect) said about using context :



“My personal opinion is that the new context is ready to be used for low-frequency, unlikely updates (such as localization or theme). It can also be used in all cases where the old context was used, i.e. for static values ​​with subsequent distribution of the update by subscription. It is not ready to be used as a replacement for Flux-like state distributors. "



There are many articles on the web that recommend setting up multiple separate contexts for different parts of the state, avoiding unnecessary re-renders and solving scoping issues. Some of the posts also suggest adding your own "contextual components" , which requires a combination of React.memo (), useMemo (), and neatly splitting the code into two contexts for each part of the application (one for data, one for update functions). Of course, you can write code this way, but in this case you are reinventing React-Redux.



So while Context + useReducer () is a lightweight alternative to Redux + React-Redux in a first approximation ... these combinations are not identical, context + useReducer () cannot completely replace Redux!



Choosing the right tool



In order to choose the right tool, it is very important to understand what tasks the tool solves, as well as what tasks you face.



Use Cases Overview



  • Context

    • Transfer of data to nested components without "drilling"


  • useReducer ()

    • Controlling the state of a complex component using a reducer function


  • Context + useReducer ()

    • Managing the state of a complex component using a reducer function and transferring state to nested components without "drilling"


  • Redux

    • Controlling a very complex state with reducer functions
    • Traceability of when, why and how the state changed over time
    • Desire to completely isolate the state management logic from the UI layer
    • Distributing state management logic between different UI layers
    • Using the capabilities of middleware to implement additional logic when initializing operations
    • The ability to save certain parts of the state
    • Ability to get reproducible error reports
    • Ability to quickly debug logic and UI during development


  • Redux + React-Redux

    • All use cases for Redux + the ability for React components to interact with the Redux store




Once again: the named tools solve different problems!



Recommendations



So how do you decide what to use?



To do this, you just need to determine which tool best solves the problems of your application.



  • If you just want to avoid "drilling", use context
  • , , + useReducer()
  • , , .., Redux + React-Redux


I believe that if your application has 2-3 contexts for managing state, then you should switch to Redux.



You will often hear that “using Redux involves writing a lot of boilerplate code,” however, “modern Redux” makes it a lot easier to learn and use. The official Redux Toolkit solves the templating problem, and the React-Redux hooks make it easy to use Redux in React components.



Of course, adding RTK and React-Redux as dependencies increases the application bundle over the context + useReducer (), which are built-in. But the advantages of this approach cover the disadvantages - better state tracing, simpler and more predictable logic, improved component rendering optimization.



It's also important to note that one does not exclude the other - you can use Redux, Context, and useReducer () together. We recommend storing "global" state in Redux and local state in components, and be careful about determining which part of the application should be stored in Redux and which in components.... So you can use Redux to store global state, Context + useReducer () to store local state, and Context to store static values, all simultaneously in the same application.



Once again, I'm not arguing that all application state should be stored in Redux, or that Redux is always the best solution. My point is that Redux is a good choice, there are many reasons to use Redux, and the fees to use it are not as high as many think.



Finally, context and Redux are not one of a kind. There are many other tools that address other aspects of state management. MobX is a popular solution that uses OOP and observables to automatically update dependencies. Other approaches to state renewal include Jotai, Recoil, and Zustand. Data libraries like React Query, SWR, Apollo, and Urql provide abstractions that make it easy to use common patterns for working with server-cached state (a similar library ( RTK Query ) will appear for the Redux Toolkit soon ).



I hope this article helped you understand the difference between context and Redux, and which tool should be used and when. Thank you for attention.



All Articles