Redux vs React's Context API
For years, Redux was the most popular solution for global state management in React apps. Will the Context API change that?
Redux vs React's Context API
For the last few years, Redux has been THE state management solution for bigger React apps.
It's far from being dead and yet, a strong enemy is arising: React's Context API!
In this article:
What is Redux?
Redux is used to manage the state of a React app in a centralized place. "State" simply refers to data you need to render the user interface correctly. Examples would be:
Products in a shopping cart
The information whether the user is waiting for a Http request to finish
Technically, Redux is not limited to usage in React apps - and indeed there are implementations in other technologies, too (e.g. NgRx for Angular). But Redux is particularly popular for React.
It consists of four main building blocks:
A single, centralized state (i.e. a global JS object you could say) which is not directly accessible or mutable
Reducer functions that contain the logic to change and update the global state (by returning a new copy of the old state with all the required changes)
Actions that can be dispatched to trigger a reducer function to run
Subscriptions to get data out of the global state (e.g. to use it in React components)
For a more detailed explanation of how Redux works, please visit the official docs.
In a React app, you typically have a couple of actions or action creators, for example (with redux thunk being used to support asynchronous actions):
export const ADD_PRODUCT_TO_CART = 'ADD_PRODUCT_TO_CART'export const REMOVE_PRODUCT_FROM_CART = 'REMOVE_PRODCUT_FROM_CART'export const addProductToCart = product => {return dispatch => {setTimeout(() => {dispatch({type: ADD_PRODUCT_TO_CART,payload: product,})}, 700)}}export const removeProductFromCart = productId => {return dispatch => {setTimeout(() => {dispatch({type: REMOVE_PRODUCT_FROM_CART,payload: productId,})}, 700)}}
You then also have your reducer to update your global state:
import { ADD_PRODUCT_TO_CART, REMOVE_PRODUCT_FROM_CART } from './actions'const initialState = {products: [{ id: 'p1', title: 'Gaming Mouse', price: 29.99 },{ id: 'p2', title: 'Harry Potter 3', price: 9.99 },// ...],cart: [],}const shopReducer = (state = initialState, action) => {switch (action.type) {case ADD_PRODUCT_TO_CART:// Shortened! Cart updating logic would be found here// See the example project linked abovereturn { ...state, cart: updatedCart }case REMOVE_PRODUCT_FROM_CART:// Shortened, too!return { ...state, cart: updatedCart }default:return state}}
A store would be constructed and passed to a wrapper around the root application component:
// Other imports...import { createStore, applyMiddleware } from 'redux'import { Provider } from 'react-redux'import reduxThunk from 'redux-thunk'// Other imports...import shopReducer from './store/reducers'const store = createStore(shopReducer, applyMiddleware(reduxThunk))ReactDOM.render(<Provider store={store}> { /* highlight-line */ }<App /></Provider>,document.getElementById('root'))
Any component in the app can then be connected to the global Redux store (via the react-redux package):
// Other imports...import { connect } from 'react-redux'import { addProductToCart } from '../store/actions'class ProductsPage extends Component {render() {return (<React.Fragment><MainNavigation cartItemNumber={this.props.cartItemCount} /><main className="products">{/* Shortened: Content gets rendered here! */}</main></React.Fragment>)}}const mapStateToProps = state => {return {products: state.products,cartItemCount: state.cart.reduce((count, curItem) => {return count + curItem.quantity}, 0),}}const mapDispatchToProps = dispatch => {return {addProductToCart: product => dispatch(addProductToCart(product)),}}export default connect(mapStateToProps,mapDispatchToProps)(ProductsPage) // highlight-line
The connect
method sets up a subscription behind the scenes.
That's Redux and how you use it in a React app in a nutshell. How does React's Context API work?
What is React's Context API?
React's Context API is there to solve a simple problem which you'll face in any non-trivial React app: How can you manage state which you need in multiple, not directly connected components?
Sure, you can always set up complex chains of props being passed around (i.e. pass props through multiple layers of React components).
const Button = props => (<p className={props.theme.light ? 'btn--light' : 'btn--dark'}> { /* highlight-line */ }Click me</p>);const Form = props => (<form><input type="text" /><Button theme={props.theme} /> { /* highlight-line */ }</form>);const App = props => {const [theme, setTheme] = useState('light') ;// Theme is managed herereturn (<div><Form theme={theme} /> { /* highlight-line */ }</div>)};
But passing props around like this makes your components harder to re-use since they have to handle props they don't need. It's also simply extra work.
Any change to your app's state or component structure also leads to significant refactoring work.
That's why Redux became popular, it solves that problem! It also helps with routing where we might not be using our components via JSX.
<Route path="/users" component={Users} />// Hard to pass props to Users here, since it's not <Users />
React's Context API provides a comparable way of passing data though.
It generally consists of three building blocks:
The Context Object
import React from 'react'export default React.createContext({}) // argument is the default starting value
You can define the Context object in a separate file or right next to a component in a component file. You can also have multiple Context objects in one and the same app.
But what IS Context actually?
In the end, it's just some data - e.g. a JavaScript object (could also be just a number or string etc) which is shared across component boundaries. You can store any data you want in Context.
For this, it's not enough to create a Context object though, you also need to provide it!
Providing Context
With the Context created, you can now provide it to all components that should be able to interact with it (i.e. read data from it or trigger a method stored in Context - more on that later).
Context should be provided in a component that wraps all child components that eventually need access to the Context.
For data that should be available in your entire app, you have to provide Context in your root component (e.g. <App />
) therefore. If you only need Context in a part of your app, you can provide it on a component a little further down the component tree.
You do provide Context like this:
// Other imports...import ShopContext from './path/to/shop-context'; // The path to the file where you called React.createContext()class App extends Component {render() {return (<ShopContext.Provider value={{products: [],cart: []}}>{/* Any child or child of a child component in here can access 'ShopContext'*/}</ShopContext.Provider />);}}
Please note the value
prop on <ShopContext.Provider>
: The value you set here is forwarded to the wrapped child components. And if the value
changes, it will also change in the child components.
The fact that updates to the data passed to value
are received by consumers of our Context and allow us to use the Context API as a global state management tool.
3. Consuming Context
For that, let's first of all have a look at how other components can consume context. We got two options:
3.1 Using Context.Consumer
// Other imports...import ShopContext from '../context/shop-context'class ProductsPage extends Component {render() {return (<ShopContext.Consumer> { /* highlight-line */ }{context => (<React.Fragment><MainNavigationcartItemNumber={context.cart.reduce((count, curItem) => {return count + curItem.quantity}, 0)}/><main className="products">...</main></React.Fragment>)}</ShopContext.Consumer>)}}export default ProductsPage
<ShopContext.Consumer>
is a wrapper component we can use to "inject" the Context provided in some parent component (doesn't have to be the immediate parent) in this child component.
The context
is received as an argument to a function which you pass as a direct child to <ShopContext.Consumer>
. That's important! You don't (directly) place JSX between <ShopContext.Consumer> ... </ShopContext.Consumer
.
The context
object is the exact same object passed to value
on our <ShopContext.Provider>
- this means that when the data passed to value
changes, the context
object in the child component also changes and hence this child component updates. That's a nice form of updating different components based on some centralized state.
3.2 Using static contextType
Besides using <ShopContext.Consumer>
, we can also get access to our Context by setting a static property in our (class-based) child component. Important: Unlike <ShopContext.Consumer>
, which you can use in functional and class-based components, this method will only work in class-based ("stateful") components!
Side note: With React Hooks you also got a way of tapping into Context anywhere in functional components, too. See part 2 of this article.
// Other imports...import ShopContext from '../context/shop-context'class CartPage extends Component {static contextType = ShopContext // highlight-linecomponentDidMount() {// Advantage of static contextType: We can now also access Context in the rest of the componentconsole.log(this.context)}render() {return (<React.Fragment><MainNavigationcartItemNumber={this.context.cart.reduce((count, curItem) => {return count + curItem.quantity}, 0)}/><main className="cart">...</main></React.Fragment>)}}export default CartPage
React gives you a this.context
property in your class-based components if you set static contextType = YourContext
in the component. The big advantage is, that - unlike as with the previous approach - you can now use the Context object anywhere in your component (including places like componentDidMount
).
Updating State via Context
Thus far, we're:
Providing Context in
App.js
(or basically any component that wrap the components that want to work with the Context data)Consuming Context in the components that need to get Context data (or change it)
How DO we now change and update the Context data?
Remember, that we pass an object as a value
to our Context provider:
<ShopContext.Providervalue={{products: [],cart: [],}}>...</ShopContext.Provider>
Since this happens in a normal React component - e.g. App.js
with the <App />
component - we can use the normal React state management solution to update the data in that component => state
and setState()
(or, covered in part 2, useState()
).
Hence, you could adjust your <App />
component like this:
import React, { Component } from 'react';import ShopContext from './shop-context';class App extends Component {state = {products: [{ id: 'p1', title: 'Gaming Mouse', price: 29.99 },{ id: 'p2', title: 'Harry Potter 3', price: 9.99 },...],cart: []};addProductToCart = product => {const updatedCart = [...this.state.cart];// Logic to update the cart correctly// See finished code (at the beginning of the article)this.setState({ cart: updatedCart });};removeProductFromCart = productId => {const updatedCart = [...this.state.cart];// Logic to update the cart correctly// See finished code (at the beginning of the article)this.setState({ cart: updatedCart });};render() {return (<ShopContext.Providervalue={{products: this.state.products,cart: this.state.cart,addProductToCart: this.addProductToCart,removeProductFromCart: this.removeProductFromCart}}>...</ShopContext.Provider>);}}export default GlobalState;
In this snippet, we're working with state just as we would in a component that's not using Context at all.
We have methods to update the state, we got a default state
property with some initial data and we then pass data from the state as well as references to our state-updating methods into our Context object (passed to value
).
The part with the two methods is particularly interesting.
Since we pass references to these methods into the Context object and this object is then accessible by all consumers of the context, we can call these methods from any component that is connected to our context.
The methods will then run in the <App />
component though, hence the state will be updated in that component - and the updated state is then passed back into child components via the Context.
That's how global state management works when using React Context instead of Redux.
Will React's Context API replace Redux?
That is a tough question.
Indeed, there are reasons that hint towards React Context being better than Redux.
It's built into React and you therefore need no extra third-party dependencies - a smaller bundle and improved project maintainability are the result. The API is also relatively straight-forward to use once you got the hang of it (especially when using hooks, see part 2). You also don't need a package like redux-thunk
to handle asynchronous actions.
Redux doesn't offer an immediate, obvious advantage other than the ability to add in middleware maybe.
But there is an important gotcha!
The Context API (currently) is not built for high-frequency updates (quote of Sebastian Markbage, React Team), it's not optimized for that. The react-redux
people ran into this problem when they tried to switch to React Context internally in their package.
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. --- Sebastian Markbage
So for the moment, it seems like you might want to look into using React Context for low-frequency updates (e.g. theme changes, user authentication) but not use it for the general state management of your application.