Simplifying Global State with React Hooks

Build a minimalist global state management solution with useContext and useReducer

Pete Givens
5 min readDec 6, 2018
Looky looky, I got Hooky

Intro

In this tutorial we will be building a minimalist global state management solution using React hooks.

We’ll create a store to hold the data as the source of truth. We’ll connect that store to a Context.Provider at the root of our application to feed the data from our store, and we’ll consume that data in our components with the useContext hook.

To update the store, we’ll define a reducer function and utilize the useReducer hook. The end result will be a highly reusable component that can be dropped anywhere in our application, and without being passed any props, has access to our global state.

Background

I’m not an expert in state management, but I think there are basically two options for sharing state in React. You can pass props downward through the tree from parent to child, or you can use React’s Context. A little while ago, this almost always meant utilizing a state management library such as Redux, MobX, unstated, or Apollo Link State.

Recently, with v16.3.0, React released a new first-class Context API. State management libraries like Redux were already implemented on the previous API, but it was undocumented and experimental. With the new official Context API, the React team made it very easy to share global state.

Even more recently, with v16.7.0-alpha, React introduced hooks, which are getting a ton of attention and for good reason. We’re going to combine these two great features in our project today.

The Project

So let’s say we have this simple Counter on our site. Users love clicking the plus or minus button and watching the number go up or down. A highly requested feature is that the counter persists its current count everywhere on the site.

Let’s say our app structure looks like this:

root
|--public
| |-index.html
|--src
| |--pages
| | |-HomePage.js
| | |-AnotherPage.js
| |-index.js
| |-Counter.js

Create a store

Our first step is to create a store to house the data. Let’s create a store.js in our src directory. In it, let’s set our initial state.

export const initialState = { count: 0 }

Next, we’ll define our reducer. If you haven’t used them before, reducers are more or less giant switch statements. They should always be pure — they should never alter the input data or depend on any external data. This ensures that it always returns the same value from the same input.

export const reducer = (state, action) => {
switch (action.type) {
case "reset":
return initialState
case "increment":
return { count: state.count + 1 }
case "decrement":
return { count: state.count - 1 }
default:
return state
}

Our reducer accepts two arguments, the current state and an incoming action. Convention is for the action to be an object that contains a type property. The reducer switches on that type to determine how to transform state. We are handling three types of actions above: increments, decrements and resets.

The last thing we’ll do in our store is create a context object.

export const Context = React.createContext()

Provide Global State

Now let’s go to the root of our application. We need to feed the data from our store to the application so that we can hook into it from our components. To do that, we’ll wrap our application in a Context.Provider

import React, { useReducer } from 'react'
import {
Context,
initialState,
reducer
} from './stores/counterStore'
function App() {
const [store, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ store, dispatch }}>
{/* <Main.js /> or whatever */}
</Context.Provider>
)
}

useReducer takes two arguments: the reducer we defined in our store and the store’s initial state. It returns to us an array containing the current state from the store and a dispatch method that we’ll use to dispatch actions to the store.

Consume State and Dispatch Actions

Finally, we’re ready to consume the state that is now globally available. Let’s take a look at our existing Counter component.

class Counter {
state = {
count: 0
};
increment = () =>
this.setState(prevState => ({ count: prevState.count + 1 }));
decrement = () =>
this.setState(prevState => ({ count: prevState.count - 1 }));
reset = () => this.setState({ count: 0 }); render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.reset}>Reset</button>
</div>
);
}
}

We’ll utilizeuseContext to hook into the context provider wrapping our application. This will give us access to the current count as well as the dispatch method we’ll use to dispatch actions to our reducer. All of the methods we have on the class now can be replaced with dispatch.

Here’s our Counter after the changes:

const Counter = () => {
const { store, dispatch } = useContext(Context)
return (
<div>
<p>You clicked {store.count} times</p>
<button onClick={
() => dispatch({ type: "increment" })
}>+</button>
<button onClick={
() => dispatch({ type: "decrement" })
}>-</button>
<button onClick={
() => dispatch({ type: "reset" })
}>Reset</button>
</div>
);
};

And we’re done! We can now drop this component anywhere on any page of our site and it will have access to the global state of our counter. We don’t need to enhance that page with an HoC or map state to props, it’s just there.

We were able to refactor our Counter into a functional component and decouple the logic from the presentation. Now we have a clearer separation of concerns and we’ve made our app more extensible. Perhaps in the future, we’ll implement a feature that punishes users for bad behavior by decrementing their hard-earned count by 5 when they click on the wrong button. All we’d have to do is add a new action type to our reducer and hook into the store in some other component — two or three lines of code.

Conclusion

I hope this was helpful and easy to follow. I’m having fun experimenting with hooks — feedback and corrections are always welcome!

Here’s a codesandbox with the final working code. Thanks for reading!

Final code with styles and page routing

--

--