DEV Community

loading...
Cover image for Replace Redux with Hooks and the Context API: how to

Replace Redux with Hooks and the Context API: how to

Riccardo Coppola
I help companies with creating better web applications. Certified Level 3 Personal trainer, nutrition geek and biohacker, trained barista and (very) amateur photographer.
Originally published at onefiniteloop.io ・5 min read

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);

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);

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 it.

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 supports 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 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 snippet puts it all together in the same file to make it easier to read and understand:

A working example

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

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 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 the "right way" iteration after iteration, in true agile spirit.

This article was originally published on onefiniteloop.io.

Discussion (11)

Collapse
iarmankhan profile image
Arman Khan

Great article. Got a nice refresh of context API. Thanks

Collapse
ricca509 profile image
Riccardo Coppola Author

Thanks Arman

Collapse
s0xzwasd profile image
Daniil Maslov

Thank you for the article! This is a new fresh look at old problems and an option to solve them.

Collapse
ricca509 profile image
Riccardo Coppola Author

Glad you found it useful Daniil.

Collapse
chathula profile image
Chathula Sampath

awesome guide! love it!

Collapse
jdmg94 profile image
José Muñoz

just FYI, when working with providers make sure the child is a pure component (if its functional that means it's wrapped in React.memo)

Collapse
ricca509 profile image
Riccardo Coppola Author

Hi,
thanks for your comment, could you please elaborate on that?

Thanks!

Collapse
jdmg94 profile image
JosĂ© Muñoz • Edited

Let me borrow some credibility

Thread Thread
ricca509 profile image
Riccardo Coppola Author • Edited

That makes sense now, thanks!

Collapse
dastasoft profile image
dastasoft

Great article!

The downside I see replacing Redux is you will lose the debug tools that comes with Redux which are very handy, but for small-medium projects maybe it's the best solution.

Collapse
ricca509 profile image
Riccardo Coppola Author

Thanks!
You're right, to some extent you can debug React Hooks and see what the state looks like, but you don't get the nice actions debug/log and the timeline (not unless a bit of development).

I guess it can be ok with small projects as you say, with bigger ones the number of actions and interactions may require better debugging.