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
2export const initialState = {
3 data: null,
4};
5
6const 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
21export 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
2import React, { createContext, useReducer, useContext } from 'react';
3import reducer, { initialState } from './reducer';
4
5const Store = createContext();
6
7const 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
12export { 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:

  • dispatch: the function that lets us dispatch Redux action
  • 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
2const 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

1import React, { useContext } from 'react';
2import { Store, Provider } from './store';
3
4const 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
10const 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
29const 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
44export 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
2const mapStateToProps = (state, props) => ({
3 message: `${state.data} ${props.extra}`,
4});
5
6const mapDispatchToProps = dispatch => ({
7 get: () => dispatch({ type: 'FETCH_DATA', payload: 'Hello world!' }),
8 reset: () => dispatch({ type: 'RESET_DATA', payload: 'null' }),
9});
10
11const ConnectedData = connect(
12 mapStateToProps,
13 mapDispatchToProps
14)(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:

  • the dispatch function
  • the objects returned by mapStateToProps and mapDispatchToProps

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

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

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
2const compose = (...funcs) => x =>
3 funcs.reduceRight((composed, f) => f(composed), x);
4
5const 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
2const customMiddleware = store => next => action => {
3 console.log('Action Triggered');
4 console.log(action);
5 next(action);
6};
7
8// ...
9
10const 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 ):

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:

If you liked this article, don't forget to share it or click here to leave a comment discuss about it on Twitter. Do you have any questions, comments or simply wish to contact me privately? I’m always reachable on Twitter or on my website!


Have a wonderful day.
Maxime

© 2019 Maxime Heckel. Made in SF.