Combining React Hooks to build a practical State Management Engine

Aldi Zhupani
5 min readApr 17, 2020

I want to start by thanking the react team for making functional programming fun again :) Long gone are the days where you need a state management platform to go along with your react app (at least for single page applications). For more complex scenarios (e.g showing notifications for different routes/pages REDUX comes in handy) In this article i would like to walk through a few examples that helped me go a little bit beyond the basics in understanding hooks.

We will focus on these three hooks and how to combine them to build a state management platform in react 16.

  • useReducer
  • useContext
  • useEffect

Just to get our feet wet let us start with a simple reducer straight out of the react documentation.

const initialState = {count: 0};import React, { useReducer } from "react";const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

The syntax is very similar to the redux library since under the hood they are doing exactly the same thing, integrating browser local storage into the app. However the big difference here is that we do not need to import additional middleware to make it happen.

So, why not just use useState?

It really depends on two factors. How many variables do you need to track. And more importantly do these variable share a common state. If counter was the only variable you need then by all means useState is the way to go, as shown in the example above. But let’s say the component needs to track 5 or more variables. Then it becomes cumbersome to maintain them as separate states, imagine a recipe component:

const [eggs, setEggs] = useState(0)
const [butter, setButter] = useState('100g')
const [sugar, setSugar] = useState('200g')
const [bakingDishes, setBakingDishes] = useState(1)
....

You can see where this is going. These have a common thread which is that they are used to bake a cake. So why not have a common cake state and dispatch actions to update its state? Something along these lines

const initialState = [
{ name: 'flour', amount: '1kg' },
{ name: 'eggs', amount: 2}
...
]

const reducer = (state, action) => {
switch (action.type) {
case "add":
return [
...state,
{
id: state.length, // add to the end of the list
amount: action.name
}
];
case "reset":
return [];
default:
return state;
}
}

So as we can see from this example sometimes it makes more sense to use useReducer hook instead of useState.

Great now that we feel comfortable with useReducer we are ready to use it as a global state manager right? Well typically hooks can only be used within the context of the functional components where they are defined hence the name. But lucky for us we can create custom hooks that can be quite useful like sharing global state. That is what we will be discussing next.

Suppose we have two different type of components, a simple a themed button and a click counter. Maybe not the the most useful but they more for illustration purposes. You can think of a typical medium application for example. We need to track the number of claps on medium, user’s theme choice and other preferences that would provide the building blocks for creating a user profile.

themedButton.js

import React from "react";
import { useStateContext } from "./stateContext";
const ThemedButton = () => {
const [{ theme }, dispatch] = useStateContext();
const currentColor = theme.primary;
const newColor = currentColor === "orange" ? "yellow" : "orange";
return (
<div style={{ border: "solid black 1px", marginBottom: 20 }}>
<p>Themed Button</p>
<div style={{ marginBottom: 10 }}>
<button
style={{ backgroundColor: `${theme.primary}` }}
onClick={() =>
dispatch({
type: "changeTheme",
newTheme: { primary: newColor }
})
}
>
{currentColor === "orange" ? "Make me yellow!" : "Make me orange!"}
</button>
</div>
</div>
);
};
export default ThemedButton;

As you may have guessed by the signature of line 5, we are dealing with a hook. Except that in this case it is a custom hook. We will dive into its contents a bit later. So essentially we are asking the parent component to give us access to the theme state and a way to update it (dispatch). Again syntax wise it is similar to redux but we don’t have to worry about injecting any middleware.

clapsCounter.js

import React from "react";
import { useStateContext } from "./stateContext";
const Counter = () => {
const [{ count }, dispatch] = useStateContext();
return (
<React.Fragment>
<div style={{ border: "solid black 1px", marginBottom: 20 }}>
<p>Counter</p>
<button
onClick={() =>
dispatch({
type: "increment"
})
}
>
Increment me!
</button>
<div style={{ marginTop: 10 }}>Count: {count}</div>
</div>
</React.Fragment>
);
};
export default Counter;

This is simply a counter that keeps track of the number of claps. Again we are accessing and updating the shared state through the custom hook in line 5.

stateContext.js

import React, { createContext, useContext, useReducer } from "react";const StateContext = createContext();export const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
export const useStateContext = () => useContext(StateContext);

And now the custom hook. There is a lot going on here but it is essentially all you need for state management.

  • createContext() is the context api (already part of react library) that provides us with Consumer and Provider namespaces. As you can imagine the former is meant to be used by the child component. The latter is used to set and update the shared context (state).
  • useReducer() is a redux like react hook that provides a way to update the state as noted earlier. But here is where the magic happens. useReducer in this case is passed as a value to the Provider. This way it can be accessed by any component that is wrapped with Provider.
  • useContext() takes an optional parameter (a context) which in our case it is the custom context we overloaded with a reducer.

Thats concludes this article. Hope some of you found it useful and again thank you for your time. As this is my first Medium article any feedback would be greatly appreciated. Happy coding!

--

--

Aldi Zhupani

Senior Software Engineer, Soccer fan, AVID learner