VictorOps is now Splunk On-Call! Learn More.

React Context vs. React Redux

Nia Watts November 05, 2018

DevOps
React Context vs. Redux - Can't Kill Redux Just Yet Blog Banner

With the advent of React 16.3, the Context library has finally been properly released and documented. Since its release, I have heard “Why are we still using Redux?” or “Let’s just use Context!” plenty of times.

Redux and Context both seem to solve the same problem – they both expose a global state. Many people seem to gravitate toward Redux due to it being verbose and allowing for extensibility. But, most of this is supported indirectly in Context.

For our examples, we’re going to make a counter and a theme object. We also assume you have a basic understanding of React and have a recent version of npm installed. We’re going to build the Redux example first and then convert our application to use Context.

I have also provided a git repo with the complete versions of both examples at: https://github.com/splunk/react-redux-context-examples.

Setup

Let’s start with a React boilerplate using create-react-app:

npx create-react-app redux-example
cd redux-example

For our Redux example, we’ll need to install 2 dependencies.

npm install --save redux react-redux

Building a Redux Application

Redux is what our store will depend on, while React Redux is what allows us to bind data to our components’ props.

With this, we should be set to make a basic React Redux application. Let’s start with the counter. Create an actions.js file in the src/ folder. In Redux, actions are plain JavaScript objects that describe changes to the store. They always contain a type, which describes the behavior of the action. Actions can optionally include other parameters which can be accessed later on. We’re going to add 2 actions, one to increase the counter and one to decrease the counter. We aren’t going to need to pass a payload because our reducer is only going to change by one.

// Action Types
export const COUNTER_INCREMENT = 'COUNTER_INCREMENT'
export const COUNTER_DECREMENT = 'COUNTER_DECREMENT'

// Action Creators
export function incrementCounter () {
  return {
    type: COUNTER_INCREMENT
  }
}

export function decrementCounter () {
  return {
    type: COUNTER_DECREMENT
  }
}

With this, we have all of our counter actions. Our action types are what the reducer is going to consume to tell us what functionality we need to use, and our action creators are what we’re going to use to construct the payload we dispatch to the store.

We also need to add reducers and our store still. Our reducer is what’s going to handle any changes to our store. For the counter example, it’s going to be very simple, but it’s best practice to never mutate the store and to replace it instead.

Create a reducers.js file. This is where most of our store logic is going to live. First, we’ll need to create our initial store. For our counter, we only need one variable.

const counterInitialState = {
  count: 0
}

Our reducer function will also need to be constructed. This is where most of our logic around our store will exist.

import {
  COUNTER_INCREMENT,
  COUNTER_DECREMENT
} from './actions'

const counterInitialState = {
  count: 0
}

function counterReducer (state = counterInitialState, action) {
  switch (action.type) {
    case COUNTER_INCREMENT:
      return Object.assign({}, state, { count: ++state.count })
    case COUNTER_DECREMENT:
      return Object.assign({}, state, { count: --state.count })
    default:
      return state
  }
}

You’ll notice we’re calling Object.assign. This is to avoid mutating the state in favor of crafting a new state. Immutable.js can make this much simpler and more reliable. For this example, we’re going to use plain JavaScript objects. At the bottom of the file, we need to export our new reducer. This will be connected to our store.

export default counterReducer

With this, our counter reducer is complete. At this point, we just need to construct the store and hook up all the components. Create a new file: store.js. Add the following to the file:

import { createStore } from 'redux'
import reducers from './reducers'

export default createStore(reducers)

In the app.js file, add the following imports:

import { Provider } from 'react-redux'
import store from './store'

We also need to add the Provider to the app. Add the Provider component to the app so it looks like this:

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <div className="app">
          Redux Example
        </div>
      </Provider>
    )
  }
}

Now, we’re going to build a new component to consume the counter in a new file: counter.js.

import React, { Component } from 'react'
import { connect } from 'react-redux'

const mapStateToProps = state => ({
  counter: state.count
})

class Counter extends Component {
  render () {
    return (
      <div className="counter">
        Counter: <span>{this.props.counter}</span>
      </div>
    )
  }
}

export default connect(mapStateToProps)(Counter)

mapStateToProps is a function that will allow our component to receive props directly from the Redux store. This component will show us the current counter value, but there’s no way to change it. We’ll need to import the action creators to get started.

import {
  incrementCounter,
  decrementCounter
} from './actions'

We can also create a function called mapDispatchToProps. This will allow us to change the state.

const mapDispatchToProps = dispatch => ({
  increment: () => dispatch(incrementCounter()),
  decrement: () => dispatch(decrementCounter())
})

Add the mapDispatchToProps to the connect call. It should look like this:

export default connect(mapStateToProps, mapDispatchToProps)(Counter)

We also need a way to call the dispatches from the user’s view. To do this, we’ll make 2 new buttons. Each one will have a dispatch tied to their onClick function. Add the following inside the return call below the Counter: <span>{this.props.counter}</span> line.

<div>
  <button onClick={ this.props.decrement }>-</button>
  <button onClick={ this.props.increment }>+</button>
</div>

With this, our counter should be fully functioning. We still need to add it to the main component though. Back in app.js, import the counter component and display it.

import Counter from './counter'
class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <div className="app">
          Redux Example
          <Counter />
        </div>
      </Provider>
    )
  }
}

Now we have a fully functioning counter application. It seems like a lot of effort to create a single counter, but with more reducers and actions, the complexity doesn’t increase by much. This is a fairly safe and reliable pattern to follow. If you run npm run start in the root of the repo, you should now see our fully functioning application!

Switching to React Context

React introduced a finalized version of their Context API in React 16. At first glance, this new Context API seems similar to how Redux works, but they operate quite differently. Instead of actions, reducers, and a store, the Context API uses Providers and Consumers. Both Providers and Consumers are higher order components, wrapping a consuming component. Providers contain all the state, alongside the functions to change it. This contrasts Redux, which separates the store into actions and reducers. This keeps things more tightly coupled, but you also don’t lose scope across files.

We’re going to convert our Redux application to use the new Context API. Make sure that your React server is not running, as we’re going to take some destructive steps now. In the root of your directory, run npm uninstall --save redux react-redux. This will clear out the unneeded dependencies for our Context example. In app.js, we no longer need the store or the Provider. Remove the imports and the <Provider store={store} /> component from the file. With this, we are no longer serving the store. We also need to create a new file to contain our Context object. You can now delete your store.js, reducers.js, and actions.js files. Create a file context.js and add the following lines:

import { createContext } from 'react'

export const { Provider, Consumer } = createContext({
  count: 0,
  increment: () => {},
  decrement: () => {}
})

This creates a Context object with some defaults. createContext returns an object with 2 parameters: Provider and Consumer. We won’t actually use these defaults, but it’s good practice. They’re used in case there is no Provider above a Consumer in the tree. We actually need to recreate these in app.js as a part of its state. In app.js, import the new Context using import { Provider as CounterProvider } from './context'. At the top of the class, add the following lines:

state = {
  count: 0,
  increment: () => { this.setState({count: this.state.count + 1}) },
  decrement: () => { this.setState({count: this.state.count - 1}) }
}

These are replacing our actions, reducers, and initial state. In most applications, you’ll want to do this in a higher order component and not in your main component. We also need to add our Provider. Surround the returned JSX with your Provider and pass it this.state as a value prop. You can also change the text in this to say “Context Example” if you’re so inclined. This won’t have any functional difference. Your app.js should now look like:

import React, { Component } from 'react'
import Counter from './counter'
import { Provider as CounterProvider } from './context.js'

import './app.css'

class App extends Component {
  state = {
    count: 0,
    increment: () => { this.setState({count: this.state.count + 1}) },
    decrement: () => { this.setState({count: this.state.count - 1}) }
  }

  render() {
    return (
      <CounterProvider value={this.state}>
        <div className="app">
          Redux Example
          <Counter />
        </div>
      </CounterProvider>
    )
  }
}

export default App

Now that the Provider is all set up, open your counter.js file to set up a Consumer object. Consumers, like Providers, are a higher order component. The implementation of a Consumer will be very similar to the implementation of a Provider. But first, we still have to do some clean up in this file. Remove the imports from the actions.js file and the react-redux import. Import the Consumer as CounterConsumer. Delete the mapStateToProps and mapDispatchToProps functions. Also, change your default export to only export your component and ensure it’s no longer using connect. That should look like: export default Counter.

Surround your JSX in this file with the Consumer now. There is one major difference though. Consumer’s children are not flat JSX, but rather a function that returns JSX. The parameter of that function is the contents of the Context’s value. This format generally looks like:

<CounterConsumer>
  {
    (state) => (
      // JSX Content
    )
  }
</CounterConsumer>

We can decompose the state parameter into our count, increment, and decrement objects though. This should make the JSX Content more verbose. We also need to change where the count, increment, and decrement functions are coming from to point to the parameters of the new function. That should look like:

<CounterConsumer>
  {
    ({count, increment, decrement}) => (
      <div className="counter">
        Counter: <span>{ count }</span>
        <div>
          <button onClick={ decrement }>-</button>
          <button onClick={ increment }>+</button>
        </div>
      </div>
    )
  }
</CounterConsumer>

The class should look like this when you are complete:

import React, { Component } from 'react'
import { Consumer as CounterConsumer } from './context'

class Counter extends Component {
  render () {
    return (
      <CounterConsumer>
        {
          ({count, increment, decrement}) => (
            <div className="counter">
              Counter: <span>{ count }</span>
              <div>
                <button onClick={ decrement }>-</button>
                <button onClick={ increment }>+</button>
              </div>
            </div>
          )
        }
      </CounterConsumer>
    )
  }
}

export default Counter

We should now have a fully functioning counter app that no longer uses Redux! If you run npm run start, you should now see your functioning Context application.

Conclusion

We’ve now seen examples of both Redux and Context in a working React app. Redux took much more initial work to implement, but it also maintains an immutable philosophy and maintains that strongly through its separation of actions and reducers. This seems to be highly maintainable for large projects and cases where changing state is frequent.

Context, on the other hand, tightly couples the state with the functions that change it. This seems to be quick and useful to use, but may get messy and difficult to maintain when you are changing state frequently, or working with larger applications. When making sure that children can access data that should rarely, if ever, change, Context does the job quickly and simply.

I have provided code examples at: https://github.com/splunk/react-redux-context-examples

Make On-Call Suck Less.

Let us help you make on-call suck less.

Get Started Now