Implement Redux-like Global Store With React Hooks and React Context

In this article, I'm going to introduce a new way to implement a global store step by step by using React Hooks and React Context. The example code is available in GitHub.

1. Initialize the Project

Let's create the project and install the dependencies.

npx create-react-app react-context-hooks
yarn install
yarn start

2. Local Store(state)

Let's illustrate React local store in a simple counter-example. There is a local state to manage the count variable, and a method — setCount to update it. It's quite straight forward, however it's restricted within the component to update count. What if we need to update from other components? React provides Context to achieve that.

JavaScript
 




x


 
1
import React, { useState } from 'react';
2
 
          
3
function Counter() {
4
  // Declare a new state variable, which we'll call "count"
5
  const [count, setCount] = useState(0);
6
 
          
7
  return (
8
    <div>
9
      <p>You clicked {count} times</p>
10
      <button onClick={() => setCount(count + 1)}>
11
        Click me
12
      </button>
13
    </div>
14
  );
15
}



3. Global Store(state)

First of all, you need to walk through a basic tutorial for React Context. In a nutshell, React Context provides a way to pass data through the component tree without having to pass props down manually at every level.

3.1 ContextProvider

We define a count state and pass the value to the global context so that its descendants can access count and setCount. As you can see, we use React hooks API: useContext. If you’re familiar with the context API before Hooks, useContext(CounterContext) is equivalent to static contextType = CounterContext in a class, or to <CounterContext.Consumer>. We also create a provider for its descendants to consume and subscribe to changes.

JavaScript
 




x


1
import React, {useState, createContext, useContext} from "react";
2
 
          
3
export const CounterContext = createContext();
4
 
          
5
export const CounterProvider = props => {
6
    const [count, setCount] = useState(0);
7
 
          
8
    return <CounterContext.Provider value={[count, setCount]} {...props} ></CounterContext>;
9
};
10
export const useCounterStore = () => useContext(CounterContext);



3.2 Consume Context

We'll create two components called counterview and countercontroller to display and update the count variable in the global store, which will leverage useCounterStore function.

CounterView:

JavaScript
 




xxxxxxxxxx
1
11


 
1
import React from "react";
2
import {useCounterStore} from "../../store";
3
 
          
4
export default function CounterView() {
5
    const [count] = useCounterStore();
6
 
          
7
    return (
8
        <p>You clicked {count} times</p>
9
    );
10
}
11
 
          



CounterController:

JavaScript
 




xxxxxxxxxx
1
12


 
1
import React from "react";
2
import {useCounterStore} from "../../store";
3
 
          
4
export default function CounterController() {
5
    const [count, setCount] = useCounterStore();
6
 
          
7
    return (
8
        <button onClick={() => setCount(count + 1)}>
9
            Click me
10
        </button>
11
    );
12
}



Main app:

We just need render CounterView and CounterController as the children of CounterProvider so that they can consume the Context.

JavaScript
 




x



1
import React from 'react';
2
import logo from './logo.svg';
3
import './App.css';
4
import {CounterProvider} from "./store";
5
import CounterView from "./components/counterview";
6
import CounterController from "./components/countercontroller";
7
 
          
8
function App() {
9
  return (
10
    <div className="App">
11
      <header className="App-header">
12
        <img src={logo} className="App-logo" alt="logo" />
13
        <CounterProvider>
14
          <CounterView />
15
          <CounterController />
16
        </CounterProvider>
17
      </header>
18
    </div>
19
  );
20
}
21
 
          
22
export default App;
23
 
          



4. Further Example: userReducer

In this chapter, we will move forward to a more complicated example in which React Context works together with useReducer API to achieve Redux-like global store. If you are not familiar with Redux,  please check my article - https://dzone.com/articles/how-to-build-a-single-page-ui-application-by-using.

4.1 Actions

We define two actions to manipulate the global store: increase and decrease

JavaScript
 




xxxxxxxxxx
1


 
1
export const increase = value => ({type: "increment", value: value});
2
 
          
3
export const decrease = value => ({type: "decrement", value: value});



4.2 Reducers

I prefer to use the generic way to define reducers, but you also can see the commented code below which is more direct.

JavaScript
 




x


 
1
const initialState = {count: 0};
2
 
          
3
// function counterReducer(state, action) {
4
//     switch (action.type) {
5
//         case 'increment':
6
//             return {count: state.count + action.value};
7
//         case 'decrement':
8
//             return {count: state.count - action.value};
9
//         default:
10
//             throw new Error();
11
//     }
12
// }
13
 
          
14
const increment = (state, { value }) => ({
15
    count: state.count + value
16
});
17
 
          
18
const decrement = (state, { value }) => ({
19
    count: state.count - value
20
});
21
 
          
22
 
          
23
const createReducer = (handlers) => (state, action) => {
24
    if (!handlers.hasOwnProperty(action.type)) {
25
        return state;
26
    }
27
    return handlers[action.type](state, action);
28
};
29
 
          
30
const counterReducerHandler = {
31
    increment, decrement
32
}
33
 
          
34
export const CounterReducer = [createReducer(counterReducerHandler), initialState];



4.3 Global Store: useReducer Together With React Context

As you can see from the code, the only difference to chapter 3 is to bind useReducer to the context provider. So it would be straight forward to understand the code.

JavaScript
 




x


 
1
import React, {createContext, useContext, useReducer} from "react";
2
import {CounterReducer} from "../reducers";
3
4
export const CounterContext = createContext();
5
6
export const CounterProvider = props => {
7
    const [state, dispatch] = useReducer(...CounterReducer);
8
9
    return <CounterContext.Provider value={[state, dispatch]} {...props} />;
10
};
11
12
export const useCounterStore = () => useContext(CounterContext);
13



4.4 Access to Global Store and Dispatch Actions

There is a slight difference to chapter 3. We can access the state and dispatch by using useCounterStore,

CounterView:

JavaScript
 




xxxxxxxxxx
1
11


 
1
import React from "react";
2
import {useCounterStore} from "../../store";
3
 
          
4
export default function CounterView() {
5
    const [state] = useCounterStore();
6
 
          
7
    return (
8
        <p>You clicked {state.count} times</p>
9
    );
10
}



CounterController:

JavaScript
 




xxxxxxxxxx
1
19


 
1
import React from "react";
2
import {useCounterStore} from "../../store";
3
import {increase, decrease} from "../../actions";
4
 
          
5
export default function CounterController() {
6
    const [, dispatch] = useCounterStore();
7
 
          
8
    return (
9
        <div>
10
            <button onClick={() => dispatch(increase(1))}>
11
                Increase me
12
            </button>
13
            <button onClick={() => dispatch(decrease(1))}>
14
                Decrease me
15
            </button>
16
        </div>
17
    );
18
}


react-context-hooks

 

 

 

 

Top