DEV Community

Sze Ying 🌻
Sze Ying 🌻

Posted on

Set up State Management using React Hooks and Context API in 3 easy steps

I've been using React Hooks and Context API to do state management for all my React side projects. As shared in one of my previous posts, I first read about this approach in this blog post and found it very comprehensive and useful. With this approach, you can set up your state management in 3 easy steps:

  1. Set up your Context
  2. Provide your components access to your Context
  3. Access your Context

The following code snippets assume we are writing a simple application that changes the colour of a circle according to user selection of the desired colour.

Simple demo application

Choose thy color!

Step 1: Set up your Context

You can think of Context as a data store, while the Provider provides access to this data store to other components.

// src/context/ColorContextProvider.jsx

import React, { createContext, useReducer } from "react";
import { colorReducer } from "./color.reducer";

// Here we initialise our Context
const initialState = { color: "red" };
export const ColorContext = createContext(initialState);

// We use the useReducer hook to expose the state and a dispatch function
// These will provide access to the Context later on
export const ColorContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(colorReducer, initialState);
  return (
    <ColorContext.Provider value={{ state, dispatch }}>
      {children}
    </ColorContext.Provider>
  );
};

Personally I also choose to set up actions and the reducer separately to emulate the Redux lifecycle. Doing so makes it easier for my mind to understand how everything connects together.

// src/context/color.actions.js

export const SET_COLOR = "SET_COLOR";
export const setColor = (color) => ({ type: SET_COLOR, data: color });
// src/context/color.reducer.js

import { SET_COLOR } from "./color.actions";

export const colorReducer = (state, action) => {
  const { type, data } = action;
  switch (type) {
    case SET_COLOR:
      return { ...state, color: data };
    default:
      return state;
  }
};

Note about the reducer function: deep equality is not regarded in the detection of state change. There will only be detection if the state object has changed. Some examples:

export const reducer = (state, action) => {
  const { type, data } = action;
  switch (type) {
    case SET_PROP: 
      // State change will be detected
      return { ...state, prop: data };
    case ADD_PROP_TO_ARRAY:
      state.arr.push(data);
      // State change will not be detected
      // as the same state object is returned
      return state;
    case ADD_PROP_TO_ARRAY_SPREAD_STATE:
      state.arr.push(data);
      // State change will be detected
      // as a different state object is returned
      return { ...state };
    default:
      return state;
  }
};

Step 2: Provide your components access to your Context

To allow components to read or write from the Context, they must be wrapped with the Context provider.

// src/App.jsx

import React from "react";
import "./App.css";
import { ColorToggle } from "./components/ColorToggle";
import { Ball } from "./components/Ball";
import { Footer } from "./components/Footer";
import { ColorContextProvider } from "./context/ColorContextProvider";
import { Header } from "./components/Header";

function App() {
  return (
    <div className="App">
      <Header />
      <ColorContextProvider>
        <ColorToggle />
        <Ball />
      </ColorContextProvider>
      <Footer />
    </div>
  );
}

export default App;

Note that we don't wrap the Header and Footer components with the ColorContextProvider, so they would not be able to access the ColorContext. This differs from Redux's global store pattern where all components in the application can access any data in the state. By providing access to the state only to the components that require it, the modularity of state management is improved.

Step 3: Access your Context

There are two parts to accessing the context -- writing and reading. Both are done using the useContext hook.

Writing to the Context

For our simple application, we update the color value in our state every time the user clicks on any of the color toggle buttons.

// src/components/ColorToggle.jsx

import React, { useContext } from "react";
import { ColorContext } from "../context/ColorContextProvider";
import { setColor } from "../context/color.actions";

export const ColorToggle = () => {
  const { dispatch } = useContext(ColorContext);
  const dispatchSetColor = (label) => dispatch(setColor(label));
  return (
    <div className="toggle ma20">
      <ColorToggleButton
        label={"red"}
        onClickHandler={() => dispatchSetColor("red")}
      />
      <ColorToggleButton
        label={"blue"}
        onClickHandler={() => dispatchSetColor("blue")}
      />
      <ColorToggleButton
        label={"yellow"}
        onClickHandler={() => dispatchSetColor("yellow")}
      />
    </div>
  );
};

export const ColorToggleButton = ({ label, onClickHandler }) => (
  <button className="ma20" onClick={onClickHandler}>
    {label}
  </button>
);

Reading from the Context

We read from the state to decide what color to render the ball in.

// src/components/Ball.jsx

import React, { useContext } from "react";
import { ColorContext } from "../context/ColorContextProvider";

export const Ball = () => {
  // Again we use the useContext hook to get the state
  const { state } = useContext(ColorContext);
  return <div className={`ball--${state.color} ma20`} />;
};

And that's it! Just 3 simple steps and we have our state management set up. The full source code is here.

Do you use a different strategy for state management in your React apps? Please do share; I would love to try something different for my next side project 🍭

Top comments (0)