Rebuilding Redux with Hooks and Context
December 4, 2018 / 8 min read
Last Updated: December 4, 2018There’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.js2export const initialState = {3data: null,4};56const reducer = (state, action) => {7const reduced = { ...state };8switch (action.type) {9case 'FETCH_DATA':10return {11...reduced,12data: action.payload,13};14case 'RESET_DATA':15return initialState;16default:17return state;18}19};2021export 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.js2import React, { createContext, useReducer, useContext } from 'react';3import reducer, { initialState } from './reducer';45const Store = createContext();67const Provider = ({ children }) => {8const store = createStore(reducer, initialState); // we'll go back to this later9return <Store.Provider value={store}>{children}</Store.Provider>;10};1112export { 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.js2const createStore = (reducer, initialState) => {3const [state, dispatch] = useReducer(reducer, initialState);4return { 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';34const Data = (props) => {5const { state, dispatch } = useContext(Store);6return <div>{props.data}</div>;7};89// An example of functional component using the useContext10const Controls = () => {11const { state, dispatch } = useContext(Store);1213return (14<div>15<button16onClick={() =>17dispatch({ type: 'FETCH_DATA', payload: 'Hello world!' })18}19>20Fetch Data21</button>22<button onClick={() => dispatch({ type: 'RESET_DATA', payload: null })}>23Reset24</button>25</div>26);27};2829const App = () => {30return (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};4344export 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.js2const mapStateToProps = (state, props) => ({3message: `${state.data} ${props.extra}`,4});56const mapDispatchToProps = (dispatch) => ({7get: () => dispatch({ type: 'FETCH_DATA', payload: 'Hello world!' }),8reset: () => dispatch({ type: 'RESET_DATA', payload: 'null' }),9});1011const 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:
- the
dispatch
function - the objects returned by
mapStateToProps
andmapDispatchToProps
Implementation of the connect HoC from Redux based on the useContext hook
1// store.js2const connect = (mapStateToProps = () => {}, mapDispatchToProps = () => {}) => (3WrappedComponent4) => {5return (props) => {6const { dispatch, state } = useContext(Store);7return (8<WrappedComponent9dispatch={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.js2const compose = (...funcs) => (x) =>3funcs.reduceRight((composed, f) => f(composed), x);45const createStore = (reducer, initialState, middlewares) => {6const [state, dispatch] = useReducer(reducer, initialState);78if (typeof middlewares !== 'undefined') {9// return middlewares(createStore)(reducer, initialState);10const middlewareAPI = {11getState: () => state,12dispatch: (action) => dispatch(action),13};14const chain = middlewares.map((middleware) => middleware(middlewareAPI));15const enhancedDispatch = compose(...chain)(dispatch);16return { state, dispatch: enhancedDispatch };17}1819return { 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.js2const customMiddleware = (store) => (next) => (action) => {3console.log('Action Triggered');4console.log(action);5next(action);6};78// ...910const Provider = ({ children }) => {11const store = createStore(reducer, initialState, [customMiddleware]);12return <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
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 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