Managing React state with Context


YouTube video:

CodeSandbox demo:

Motivation

Why use Context to handle React state? The same reason you use Redux to manage your application store. If your components talk to one another and lifting state becomes cumbersome, then Context is a native way to deal with your application state.

Prerequisites

We will be refactoring <App>—which uses local state—to use Context:

// App.js
import { useState } from 'react';

export default function App() {
  const [state, setState] = useState({ count: 0 });

  function increment() {
    setState({
      count: state.count + 1,
    });
  }

  function decrement() {
    setState({
      count: state.count - 1,
    });
  }

  return (
    <>
      <p>{state.count}</p>
      <button onClick={increment}>+</button>&nbsp;
      <button onClick={decrement}>-</button>
    </>
  );
}

As you can see, there’s a counter with 2 buttons that increments or decrements the count.

Provider

Create <Provider> and set prop value to 0:

// Provider.js
import { createContext } from 'react';

export const Context = createContext();

export default function Provider(props) {
  return <Context.Provider value={0}>{props.children}</Context.Provider>;
}

Render <Provider> as the top-level component in index.js:

// index.js
import { render } from 'react-dom';
import App from './App';
import Provider from './Provider';

render(
  <Provider>
    <App />
  </Provider>,
  document.getElementById('root')
);

Get the Provider value with useContext in <App>:

// App.js
import { useContext } from 'react';
import { Context } from './Provider';

export default function App() {
  const value = useContext(Context);
  return (
    <>
      <p>{value}</p>
      <button>+</button>&nbsp;
      <button>-</button>
    </>
  );
}

useReducer

Create a reducer function:

// reducer.js
export default function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + action.payload,
      };
    case 'DECREMENT':
      return {
        count: state.count - action.payload,
      };
    default:
      throw new Error();
  }
}

Pass state and dispatch from useReducer to Provider prop value:

// Provider.js
import { createContext, useReducer } from 'react';
import reducer from './reducer';

export const Context = createContext();

const initialState = {
  count: 0,
};

export default function Provider(props) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <Context.Provider value={[state, dispatch]}>
      {props.children}
    </Context.Provider>
  );
}

Update <App> with the new context value:

// App.js
import { useContext } from 'react';
import { Context } from './Provider';

export default function App() {
  const [state, dispatch] = useContext(Context);

  function increment() {
    dispatch({
      type: 'INCREMENT',
      payload: 1,
    });
  }

  function decrement() {
    dispatch({
      type: 'DECREMENT',
      payload: 1,
    });
  }

  return (
    <>
      <p>{state.count}</p>
      <button onClick={increment}>+</button>&nbsp;
      <button onClick={decrement}>-</button>
    </>
  );
}

Consumer

An alternative to useContext is Consumer.

First, export Consumer from Context:

// Provider.js
export const Context = createContext();
export const { Consumer, Provider } = Context;

Then replace useContext with <Consumer>:

// App.js
import { Consumer } from './Provider';

export default function App() {
  return (
    <Consumer>
      {([state, dispatch]) => (
        <>
          <p>{state.count}</p>
          <button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>
            +
          </button>&nbsp;
          <button onClick={() => dispatch({ type: 'DECREMENT', payload: 1 })}>
            -
          </button>
        </>
      )}
    </Consumer>
  );
}

<Consumer> requires a callback function for prop children. The 1st argument is the Provider value.

Class

For class components, the Provider value can be accessed on this.context:

class App extends Component {
  render() {
    const [state, dispatch] = this.context;
    // ...
  }
}

Todo

Ideas for improvements:

  • Build a custom hook for useContext(Context).
  • Refactor action types to constants.
  • Create action creators.
  • Set displayName on Context to improve visibility in React DevTools.

Resources



Please support this site and join our Discord!