Riccardo Coppola

Replace Redux with React Hooks and the Context API: how to and code

March 13, 2020

Is it possible to use the new React Context API and hooks to completely replace Redux? Is it worth it? Does it yield the same results and is the solution as easy to use as Redux + React-redux?

With the advent of the new React Context API, passing data deep down in an application became easier and with the new hooks I started to see a lot of posts advertising that replacing Redux was possible. I wanted to find out for myself, so I started looking closer at the React docs and try to build my own Redux.

The following is what I found out and what I came up with.

Context API

One of the challenges of React is how to pass props to components deep down the tree; props that are “global” to the application, that many components may want to use and usually represent configuration, UI theme, translations.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

How to use it

To start building a Redux-like library, I want to make available a state object and a dispatch function to the whole application, so let’s build an example that takes advantage of the Context API and does just that:

import React from "react";

// Create a context with a default value
const StateContext = React.createContext({
  state: {},
  dispatch: () => {},
});

const ComponentUsingContext = () => {
  return (
    // Wrap the component using the value with the context consumer
    <StateContext.Consumer>
      {({ state }) => <div>App state: {JSON.stringify(state)}</div>}
    </StateContext.Consumer>
  );
};

// Wrap your component with the provider and pass a value if you don't want to use the default
const App = () => {
  return (
    <StateContext.Provider
      value={{
        state: {
          counter: 1,
        },
        dispatch: () => console.log("dispatch"),
      }}
    >
      <ComponentUsingContext />
    </StateContext.Provider>
  );
};

The above is a quick look at how you can use the Context to send data down the components’ tree, and it doesn’t look very different from the React Redux Provider that you use to wrap your app with.

Note how you create a Context first, then use the Context.Provider to send data down into the tree and Context.Consumer to use that data at any nesting level.

The part using the Context.Consumer looks a bit more complex than I’d like, but there is a hook that makes it look at lot cleaner (more on this in a sec).

Now that we have a way to “inject” data into an app, let’s see how we can leverage hooks to build the additional features required to replace Redux.

Hooks

Hooks were introduced in React 16.8.0 to tackle different classes of problems:

  • Making it easier to reuse stateful logic between components
  • Move away from classes, their inherent verbosity and the use of this
  • Making more use of ahead-of-time compilation to create optimised code (and classes can encourage patterns that make it difficult)
  • Probably other reasons, which I am not aware of 😇

Among all the hooks that come with React, useContext and useReducer are the ones that can help build a Redux-like library in React.

useContext

const value = useContext(MyContext);

Example usage for useContext

It is an alternative to using the Context.Consumer pattern (and makes the code looks more readable in my opinion).

Let’s see it applied to the previous Context example:

import React, { useContext } from "react";

const StateContext = React.createContext({
  state: {},
  dispatch: () => {},
});

const ComponentUsingContext = () => {
  const { state } = useContext(StateContext); // <---
  return <div>App state: {JSON.stringify(state)}</div>;
};

const App = () => {
  return (
    <StateContext.Provider
      value={{
        state: {
          counter: 1,
        },
        dispatch: () => console.log("dispatch"),
      }}
    >
      <ComponentUsingContext />
    </StateContext.Provider>
  );
};

You still have to use the Context.Provider, but retrieving the values from the context looks a lot better now.

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

Example usage for useReducer

The useReducer hook accepts a reducer (same as you’d write for Redux) and an initial state and return the new state with a dispatch method.

state and dispatch are exactly what I need to pass down the application through the React.Context.

Trying to put things together

The API of my Redux-like library should include:

  • a Provider to wrap the app and inject the state and dispatch method
  • a useStore method to create a store (containing the state and dispatch method) to pass to the Provider
  • a connect method to hook a component to the state

Provider

The provider would simply be a Context.Provider:

const Context = React.createContext(); // No default needed here

export const Provider = Context.Provider;

connect

A very basic connect would accept a Component, then make use of the useContext to get the state and dispatch and then pass them to the component.

export const connect = (Component = () => {
  const { state, dispatch } = useContext(Context);

  const props = { state, dispatch };

  return React.createElement(Component, props, null);
});

This is of course a very basic version, that passes the whole state to the component: not exactly what I want.

Introducing mapStateToProps and mapDispatchToProps

The Redux connect method makes use of mapStateToProps to map the whole state to the props that the component needs.

It also uses mapDispatchToProps to pass actions wrapped by the dispatch method as props to the component.

I wanted to support those methods too, so this is an improved version, that also support the component’s own props:

export const connect =
  (mapStateToProps = () => ({}), mapDispatchToProps = () => ({})) =>
  (Component) =>
  (ownProps) => {
    const { getState, dispatch } = useContext(Context);
    const stateProps = mapStateToProps(getState(), ownProps);
    const dispatchProps = mapDispatchToProps(dispatch, ownProps);
    const props = { ...ownProps, ...stateProps, ...dispatchProps, dispatch };

    return createElement(Component, props, null);
  };

So here I added support for mapStateToProps and mapDispatchToProps, providing a default value that returns an empty object in case those arguments are not provided. I then added the dispatch method so that the component can use it to dispatch actions.

useStore

This is just a utility hook that uses useReducer to create a store and returns it, pretty much like [createStore](https://redux.js.org/api/createstore/) in Redux. It also creates a getState function that returns the state.

export const useStore = (reducer, initialState = {}) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const getState = () => state;

  return { getState, dispatch };
};

The following code puts it all together in the same file to make it easier to read and understand:

import { createElement, createContext, useReducer, useContext } from "react";

const Context = createContext();
export const ContextProvider = Context.Provider;

export const useStore = (reducer, initialState = {}) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const getState = () => state;

  return { getState, dispatch };
};

export const connect = (
  mapStateToProps = () => ({}),
  mapDispatchToProps = () => ({})
) => Component => ownProps => {
  const { getState, dispatch } = useContext(Context);
  const stateProps = mapStateToProps(getState(), ownProps);
  const dispatchProps = mapDispatchToProps(dispatch, ownProps);
  const props = { ...ownProps, ...stateProps, ...dispatchProps, dispatch };

  return createElement(Component, props, null);
};

A working example

Here’s your usual counter example using the code I just discussed (notice my CSS skills):

And a GitHub repo: https://github.com/ricca509/redux-no-redux

An important note about re-renders

You may wonder how the application re-renders, since I am never using setState, which is a requirement to trigger a re-render in React.

In Redux, the connect method triggers a [forceUpdate](https://reactjs.org/docs/react-component.html#forceupdate) when the store changes, but here?

The solution lies in how the useContext hook works:

A component calling useContext will always re-render when the context value changes.

More on this in the React docs.

Where to now?

Of course this example is not nearly as powerful as Redux is, but it proves that Redux can be replaced by Context + Hooks.

Is it the right thing to do, though? Is it the right pattern to package these new React features into a Redux-like library?

I believe that these new tools give us an opportunity to find new patterns and leverage the reusability provided by hooks to find better ways to share and access application state at any nesting level. We’ll find it iteration after iteration, in true agile spirit.


Notes on web development, life, learning and the world.