@MaximeHeckel

Rebuilding Redux with Hooks and Context

December 4, 2018 / 8 min read

Last Updated: December 4, 2018

There’s been a lot of hype recently about React Hooks and what they allow developers to achieve. Indeed, in the near future, we will be able to rely on a single React pattern to build pretty much anything we want. As of today, React consist of a lot of patterns, if not too many for some people: Stateful Classes, Functional components, Higher Order Components and render callbacks to mention just a few.
The React core team expressed several months ago their desire to slowly phase out React Classes. Hooks, along with Suspense, which I talked about in a previous post, are the main building blocks of this plan.

In this post, however, rather than focusing on how hooks impact React components themselves, I want to go a bit further and showcase how they can be used, in conjunction with the already existing Context API, to build a very basic implementation of Redux. The example I will provide covers the basics functionality of Redux for global state management.

For this example, we will consider a simple application. It will display some message that can be fetched through a Redux action FETCH_DATA which can be triggered by clicking on a button.

Provider and reducers

Let’s consider the following reducers:

Example of a classic reducer used with Redux

1
// reducers.js
2
export const initialState = {
3
data: null,
4
};
5
6
const reducer = (state, action) => {
7
const reduced = { ...state };
8
switch (action.type) {
9
case 'FETCH_DATA':
10
return {
11
...reduced,
12
data: action.payload,
13
};
14
case 'RESET_DATA':
15
return initialState;
16
default:
17
return state;
18
}
19
};
20
21
export default reducer;

As we can see, this is the kind of reducers we’re used to seeing in any Redux based application. The objective is to have the same reducers working for our implementation of Redux.

First step: Defining our **Provider**This will be the core of our reimplementation of Redux. The Redux Provider works quite like a basic React Context Provider, so we can base our work on the Context API. Our store Provider will wrap our app and let it access our store object at any level. Here’s how it looks like:

Implementation of a store provider using the React Context API

1
// store.js
2
import React, { createContext, useReducer, useContext } from 'react';
3
import reducer, { initialState } from './reducer';
4
5
const Store = createContext();
6
7
const Provider = ({ children }) => {
8
const store = createStore(reducer, initialState); // we'll go back to this later
9
return <Store.Provider value={store}>{children}</Store.Provider>;
10
};
11
12
export { Store, Provider };

Second step: **createStore **We can see above the mention of the createStore function. If you’re familiar with Redux this should ring a bell. This function takes our reducer, and the initial state object of our app returns an object with 2 essential items that are injected into the app through our Provider:

  • ArrowAn icon representing an arrow
    dispatch: the function that lets us dispatch Redux action
  • ArrowAn icon representing an arrow
    state: the object containing the global state of our app.

To reimplement this function in our example, let’s use the new React hooks. React has a very handy pre-built hook called useReducer which actually returns these 2 items stated above:

createStore implementation

1
// store.js
2
const createStore = (reducer, initialState) => {
3
const [state, dispatch] = useReducer(reducer, initialState);
4
return { state, dispatch };
5
};

We now have all the elements for our implementation of Redux to work! Below you will see the code of our basic app that is using the examples above to dispatch actions and get some data from our store.

Small application using our basic reimplementation of Redux using Context and Hooks

1
import React, { useContext } from 'react';
2
import { Store, Provider } from './store';
3
4
const Data = (props) => {
5
const { state, dispatch } = useContext(Store);
6
return <div>{props.data}</div>;
7
};
8
9
// An example of functional component using the useContext
10
const Controls = () => {
11
const { state, dispatch } = useContext(Store);
12
13
return (
14
<div>
15
<button
16
onClick={() =>
17
dispatch({ type: 'FETCH_DATA', payload: 'Hello world!' })
18
}
19
>
20
Fetch Data
21
</button>
22
<button onClick={() => dispatch({ type: 'RESET_DATA', payload: null })}>
23
Reset
24
</button>
25
</div>
26
);
27
};
28
29
const App = () => {
30
return (
31
<div className="App">
32
<Provider>
33
{/* This is an equivalent to the react-redux Provider component */}
34
<header className="App-header">
35
<h1>React {React.version}</h1>
36
<Controls />
37
<Data />
38
</header>
39
</Provider>
40
</div>
41
);
42
};
43
44
export default App;

However, we can see that although the constructs we came up with are quite similar to the ones of Redux, the way it’s used within an app is not quite the same. This is why I wanted to push the example a bit further and reimplement the connect Higher Order Component.

Rebuilding the Connect HoC

For this part, we want to achieve the following:

Example of a component using the connect HoC

1
// App.js
2
const mapStateToProps = (state, props) => ({
3
message: `${state.data} ${props.extra}`,
4
});
5
6
const mapDispatchToProps = (dispatch) => ({
7
get: () => dispatch({ type: 'FETCH_DATA', payload: 'Hello world!' }),
8
reset: () => dispatch({ type: 'RESET_DATA', payload: 'null' }),
9
});
10
11
const ConnectedData = connect(mapStateToProps, mapDispatchToProps)(Data);

Given the code above, our connect HoC must take 2 optional arguments: a mapStateToProps function and a mapDispatchToProps function. It will then inject the following items as props for the wrapped component:

  • ArrowAn icon representing an arrow
    the dispatch function
  • ArrowAn icon representing an arrow
    the objects returned by mapStateToProps and mapDispatchToProps

Implementation of the connect HoC from Redux based on the useContext hook

1
// store.js
2
const connect = (mapStateToProps = () => {}, mapDispatchToProps = () => {}) => (
3
WrappedComponent
4
) => {
5
return (props) => {
6
const { dispatch, state } = useContext(Store);
7
return (
8
<WrappedComponent
9
dispatch={dispatch}
10
{...mapStateToProps(state, props)}
11
{...mapDispatchToProps(dispatch)}
12
/>
13
);
14
};
15
};

With this implementation of connect, we now have a more familiar way to access the state from our components.

Going even further by adding middleware support

One other thing that would be nice to have in our reimplementation of Redux would be some support for middlewares. In this part will try to emulate how middlewares work in Redux, and try to end up having a similar implementation.

**How do middlewares currently work?
**In a nutshell, middlewares are enhancements to the dispatch function.
Middlewares take a store object as an argument, which contains a getState function and a dispatch function, and are then composed to finally give us an enhanced dispatch. By looking in the Redux codebase we can see that this enhanced dispatch function is a curried function where the middlewares are “composed” and then applied to our dispatch.
Compose here means that instead of having to write for example f1(f2(f3(f4))) we can simply write compose(f1,f2,f3,f4).

Note: This short summary and the code implementation below are based on my own research and on this article.

Implementation of middleware support for our createStore function

1
// store.js
2
const compose = (...funcs) => (x) =>
3
funcs.reduceRight((composed, f) => f(composed), x);
4
5
const createStore = (reducer, initialState, middlewares) => {
6
const [state, dispatch] = useReducer(reducer, initialState);
7
8
if (typeof middlewares !== 'undefined') {
9
// return middlewares(createStore)(reducer, initialState);
10
const middlewareAPI = {
11
getState: () => state,
12
dispatch: (action) => dispatch(action),
13
};
14
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
15
const enhancedDispatch = compose(...chain)(dispatch);
16
return { state, dispatch: enhancedDispatch };
17
}
18
19
return { state, dispatch };
20
};

We can now add a basic middleware to our createStore function. Here’s one that logs to the console any action that is dispatched:

Example of a custom middleware used with our Redux reimplementation

1
// store.js
2
const customMiddleware = (store) => (next) => (action) => {
3
console.log('Action Triggered');
4
console.log(action);
5
next(action);
6
};
7
8
// ...
9
10
const Provider = ({ children }) => {
11
const store = createStore(reducer, initialState, [customMiddleware]);
12
return <Store.Provider value={store}>{children}</Store.Provider>;
13
};

Conclusion

Thanks to the Context API and the recently announced Hooks, we saw that it is now easy to rebuild Redux. Is it usable? Yes, as we saw in this post, we covered the main components of Redux (Store, connect, middlewares, etc) and use them in a small app. Can this replace react-redux? Probably not. Redux still has a lot more than what we covered in this article, like the Redux Devtools or the entire ecosystem of libraries that can enhance your app on top of Redux. While writing this post I’ve personally tried to add the redux-logger middleware to our example, it “worked” but I couldn’t make it print the correct “next state”(maybe because the useReducer hook is async since it’s based on setState ):

I'm very close to have existing redux middlewares working with my implementation of Redux with React Hooks! (Here with Redux Logger, you can see the next state is not populated properly) https://t.co/HKHCPoMRUG

I'm very close to have existing redux middlewares working with my implementation of Redux with React Hooks! (Here with Redux Logger, you can see the next state is not populated properly) https://t.co/HKHCPoMRUG

but as you can see in this tweet, maybe I was just a bit too ambitious.

Want to continue working on this project or just hack on top of it? You can clone the repository containing the code featured in this article along with a basic application here.

What to read next?
If you want to read more about React or frontend development, you can check the following articles:

Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.

Have a wonderful day.

– Maxime

A simple global state management package based on React constructs