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:
- Set up your Context
- Provide your components access to your Context
- 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.
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)