Shared State with React Hooks and Context API

Shared State with React Hooks and Context API

With React 16.8 came the release of Hooks, which, along with other great functions, provides a way for function components to have a state!

In my article Managing State with React Hooks I go over how to use the hooks useState and useReducer to manage state within function components. Give this a read if you need a refresher on how hooks can help manage state.

In this article we’ll take a look at a way to use hooks along with React’s Context API to provide a shared state between function components!


Create and Provide a Context

While the Context API itself is not a part of hooks, using it along with the useReducer and useContext hooks, we can provide a reducer and state globally (or to specific component trees, depending on how you implement it) in our application.

Not familiar with React’s Context API? Check out their documentation explaining what a Context is and what it can accomplish.

Let’s look at a simple example of how this would work!

First off, we’ll create a Context that will eventually provide the state and dispatch function of a reducer to the entire application. That way the entire application will be provided a way to access the reducer and state.

import React, { createContext, useReducer } from 'react'

export const StateContext = createContext()

Build a component that provides the Context value to its children

Okay, so we have a Context created that will eventually hold the state and dispatch. How do we provide this to the entire application? We’ll need to wrap the entire application in the Provider we received from the Context. Let’s create a wrapper we can stick our app into that will provide the context value (the state and dispatcher) to its children.

import React, { createContext, useReducer } from 'react'

export const StateContext = createContext()

// This is a component that wraps all of its children in the Provider
export const StateProvider = ({ reducer, initialState, children }) => (
  <StateContext.Provider value={useReducer(reducer, initialState)}>
    { children }
  </StateContext.Provider>
)

The code above is doing a few things:

  • It exports a new component named StateProvider that will take in a reducer, an initial state, and children as props. The StateProvider component wraps all of its children in the Provider we grabbed from the StateContext we created.
  • We pass the reducer and initial state from this component’s props to the useReducer hook.
  • The Provider accepts a prop named value. This value is the data that is provided to all of the children. The useReducer hook returns an array with two items: the current state, and a dispatch function. These are the values we are passing into the value prop, thus providing those to all of StateProvider‘s child components.

Wrap the whole App in the StateProvider component

Awesome! We now have a component that will wrap all the children in a Provider that supplies them with the state and dispatch we require to share state across the entire application. Let’s wrap an app in this component so the children can consume the Context it provides.

import React from 'react';
import { StateProvider } from './State'

function StateApp() {
  // Initialize the state
  const initialState = {
    points: 0
  }

  // Defines how to update the state based on actions
  const reducer = (state, action) => {
    switch (action.type) {
        case 'add':
            return { ...state, points: state.points + 1 }
        default:
            return state;
    }
  };

  return (
    <StateProvider initialState={initialState} reducer={reducer}>
        // Application
    </StateProvider>
  )
}

export default StateApp

Here we create an initial state and a reducer to manage that state. We then wrap the app in our StateProvider component, passing in the initialState and reducer. As we saw above, StateProvider uses those to provide all of the children with state and dispatch. The result is global access to state!


Consume the Context in a Child Component

Now let’s see how a child component would access and update the global state!

In the past, if you wanted to access a Context you would need to wrap your component in that Context’s Consumer. Using the new useContext hook allows us to access the context without using the Consumer.

import React, { useContext } from 'react'
import { StateContext } from '../State'

function Points() {
  // Here, we get the state and dispatch from the context
  const [ { points }, dispatch ] = useContext( StateContext )

  return (
    <>
        {/* Clicking this button runs the global dispatch we got from the Context */}
        <button onClick={() => dispatch({type: 'add'})}>Add</button>
        <p>{ points }</p>
    </>
  );
}

export default Points

In order to access the global state we need to import the useContext hook and the StateContext we created.

We use the useContext hook to get access to the data provided by a context. This hook takes one argument, a Context, and returns the current context value. In our case, the context value is the return value of our useReducer function which contains an array of two items. The first being the current state and the second a dispatch function.

NOTE: I am using Destructuring to pull out only the points key from the state and to pull the items out of the array.

When the Add button is clicked, the global dispatch provided by the Context is fired, resulting in an increment of the global state’s points key value.

Neat! This component is now consuming and updating a global state that can be shared with other components within the StateProvider component’s component tree.


Putting it all together

Here is a full example of all of this at work. Below is the App.js file which will wrap its children in the StateProvider.

import React from 'react';
import { StateProvider } from './contexts/State'
import GlobalStateCounter from './components/GlobalStateCounter'
import './App.scss'

function App() {
  // Initialize the state
  const initialState = {
    points: 0
  }

  // Defines how to update the state based on actions
  const reducer = (state, action) => {
    switch (action.type) {
        case 'add':
            return { ...state, points: state.points + 1 }
        case 'subtract':
            return { ...state, points: state.points - 1 }
        case 'reset':
            return { ...state, points: 0 }
        default:
            return state;
    }
  };

  return (
    <div className="App">
      <StateProvider initialState={initialState} reducer={reducer}>
          {
              // Creates two instances of the component so we can see both update when state changes
              [...Array(2)].map((e, i) => <GlobalStateCounter index={i + 1}/> )
          }
      </StateProvider>
    </div>
  )
}

export default App

And here is the child component that uses the useContext hook to access the StateContext‘s value, which is an array containing the state and a dispatch function.

import React, { useContext } from 'react'
import { StateContext } from '../State'
import '../assets/scss/Points.scss'

function Points({index}) {

  const [ { points }, dispatch ] = useContext( StateContext )

  return (
    <div id="Points">
      <p className="title">Points (globalState {index})</p>
      <hr className="divider"/>
      <div className="pointsContainer">
          <div className="buttons">
            {/* These buttons use the dispatch to update the state */}
            <button className="button add" onClick={() => dispatch({type: 'add'})}>Add</button>
            <button className="button subtract" onClick={() => dispatch({type: 'subtract'})}>Subtract</button>
            <button className="button reset" onClick={() => dispatch({type: 'reset'})}>Reset</button>
          </div>
          <div className="outputBox">
            {/* Output the points variable */}
            <p>{ points }</p>
          </div>
      </div>
    </div>
  );
}

export default Points

globalStateGif-1.gif

Using React’s Context API and hooks we have implemented global state management! This is a basic example, but a strong foundation for a more complex and organized state management system.


Conclusion

Using this structure, you can provide access to a global state across your entire application! This can act as a replacement for other forms of state management like Redux. More complexity can be added to this to make it more versatile and organized, but the general idea provides a great base to start managing shared states in your function components!

Thanks for reading!