Replace redux with react hooks and context api

June 05, 2019

A post by Will Mayger
Twitter . Instagram

There is now a debate whether or not you need redux, with the release of react hooks and the context API.

Once the new redux hook API is released for public use, I think that it will still be greatly beneficial (checkout my post here about “Using redux with react hooks”), but until that happens, if you are using hooks you may want to take another approach that is ready for use right now.

Just a heads up, this does not solve everything redux will solve, however if you need a nice and easy way to manage a single state, store, or single source of truth, using reducers and actions, this will be a great little solution for you.

A little context (Pun not intended)

Firstly let’s take a quick look at the react context API. The context api helps you manage props that are passed down through many nested components.

It solves this problem by making a value visible to the entire node tree (when calling the Consumer within the Provider).

Here is a visual representation of this:

Props Problem

<Parent someProp="hi">
	<Child someProp={props.someProp}>
		<Text value={props.someProp} />
	</Child>
</Parent>

React Context Solution

const SomeContext = React.createContext({ message: 'hi' });
…

<SomeContext.Provider>
<Parent>
	<Child>
<SomeContext.Consumer>
		<Text value={props.message} />
	</SomeContext.Consumer>
</Child>
</Parent>
</SomeContext.Provider>

React hooks

React hooks makes using lifecycle events much easier and much more flexible. For a more detailed look into react hooks checkout some of my other posts such as Using window.addEventListener with react hooks or React hooks vs Classes.

Here is a short example of using a react hook:

import React, { useState } from 'react';

export default function BasicHook() {
  const [hit, setHit] = useState(0);

  return (
    <button
      onClick={() => setHit(hit + 1)}
    >
      You clicked {hit} time(s)!
    </button>
  );
}

So how does all this fit in with redux?

Redux, simply put, is a package/library that will help you manage your state. We can now do a similar thing by combining React hooks and React’s context API together to produce something that comes close to what redux will do on a basic level.

I would still recommend to use redux when a working version comes out with react hooks, but this method works really well to control your state from a single source of truth whilst making use of a dispatcher, actions and reducers.

Create your own state management

Let’s step through this bit by bit to understand how we can create our react hooks and context api state management system using the context API, and React hooks.

Firstly we need to get a fresh project started, type into your terminal npx create-react-app react-hooks-and-context-api.

Once that has been bootstrapped, cd into the project (cd react-hooks-and-context-api) and create a new folder within the src directory called store.

That should now look like:

react-hooks-and-context-api
src
store

Now let’s create our useStore hook, so create a new file called useStore.js within our store directory.

We will start by creating our context provider:

import React, { createContext, useContext } from 'react';

const StoreContext = createContext();

export const StoreProvider = ({ children }) => {
  return (
    <StoreContext.Provider value={{ test: 'test' }}>
      {children}
    </StoreContext.Provider>
  );
};

export const useStore = () => {
  const { test } = useContext(StoreContext);
  return { test };
};

Here we create two functions, the first being the react context api provider component with our values we want to pass down the tree, it accepts children as a property which means we can use it as a parent container/component.

And the second being a new react hook called useStore that enables us to reach and use the values stored within our context api provider.

Now, if we go to our src > index.js root file, we can now use this as the root component for our application like so:

import React from 'react';
import ReactDOM from 'react-dom';
import * as serviceWorker from './serviceWorker';
import App from './App';
import { StoreProvider } from './store/useStore';

ReactDOM.render(
  (
    <StoreProvider>
      <App />
    </StoreProvider>
  ),
  document.getElementById('root'),
);

serviceWorker.unregister();

As you can now see, the store provider (our react context api component) is the root/parent component for other components to be placed in, in order to make use of the context API.

To finish up the start of the context api, open up your App.js file and replace the contents with the following:

import React from 'react';
import { useStore } from './store/useStore';
import './App.css';

function App() {
  const { test } = useStore();
  return (
    <div className="App">
      <header className="App-header">
        <p>
          {test}
        </p>
      </header>
    </div>
  );
}

export default App;

As you can see in this file we are making use of the useStore react hook that is using the provider we created and implemented in the index.js file, if you now run the code, you should see the text “test” in the middle of your react application that means your application is now using react’s context API successfully!

I’m sure at this point you will be able to see how this is going to start coming together.

Next we are going to add in our redux parts. The reducers, store, dispatch, and actions.

Let’s start by creating a working store that you can dispatch actions to.

If you go back to the src > store > useStore.js file and replace the existing code with the following:

import React, { createContext, useReducer, useContext } from 'react';
import reducers from './reducers';
import { getInitialState, combineReducers } from './helpers';

const initialState = getInitialState(reducers);
const StoreContext = createContext(initialState);

export const StoreProvider = ({ children }) => {
  const [state, dispatch] = useReducer(combineReducers(reducers), initialState);
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
};

export const useStore = () => {
  const { state, dispatch } = useContext(StoreContext);
  return { state, dispatch };
};

One of the first things to look at is that we are passing our context provider some new values to keep hold of, and those values are our store object/state and our dispatch function.

This will make them available throughout our application.

We are using a built in hook from react called useReducer to get these. You can find out more about this here on the official docs.

The useReducer react hook, takes in a reducer that uses a state and an action, and then the useReducer function will give you the current state and a dispatch function to be able to update it further.

You can give it initial default values as well such as an initial state like what we are doing here in the following line.

const [state, dispatch] = useReducer(combineReducers(reducers), initialState);

You may have noticed that we are importing a function called combineReducers from a helper file which we will get to in a minute.

As you can tell from the name of it, it will combine many reducers together, similar to the function you get as part of the redux package.

Before we get to that let’s take a look at another file that we are importing called reducers.js, which contains all of our reducers.

Create a file called reducers.js within the store directory and add the following code to it:

const count = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
};

const test = (state = null, action) => {
  switch (action.type) {
    case 'CHANGE_TEST_TEXT':
      return {
        ...state,
        text: action.payload,
      };
    default:
      return state;
  }
};


export default [
  count,
  test,
];

Here we have created two different reducers and are exporting them both as an array.

A reducer is a function that will take in a state object, and an action and return the updated state or the original state depending on the action type.

An action will usually have the value of something like { type: 'ACTION', payload: 'VALUE' }.

The reducers are defining our state and our initial state in this setup, which we will look at next, but it does this by using the name of the reducer function and combining it with its state value to create a single source of truth/store.

Now we have our reducers sorted, let’s create the helper functions like the combineReducer function I mentioned earlier.

Create a new file within the store directory again, but this time it will be called helpers.js.

Within our helpers file, lets create a few helper methods for our reducers.

Add the following lines of code to the helpers.js file:

export function getInitialState(reducers) {
  const reducersCombined = {};
  for (let i = 0; i < reducers.length; i++) {
    reducersCombined[reducers[i].name] = reducers[i](undefined, {});
  }
  return reducersCombined;
}

export function combineReducers(reducers) {
  return function useReducers(state, action) {
    for (let i = 0; i < reducers.length; i++) {
      const defaultOutput = reducers[i](state[reducers[i].name], {});
      const newOutput = reducers[i](state[reducers[i].name], action);

      let st1 = defaultOutput;
      let st2 = newOutput;

      if (typeof defaultOutput === 'object') {
        st1 = JSON.stringify(defaultOutput);
        st2 = JSON.stringify(newOutput);
      }

      if (st1 !== st2) {
        const nextState = {};
        nextState[reducers[i].name] = newOutput;
        return { ...state, ...nextState };
      }
    }
    return state;
  };
}

The first function we have created, called getInitialState, takes in an array of reducers and runs through them whilst providing no state and no action, doing this will return all the default results from each reducer function, creating an initial state.

When the default state has been generated for each reducer, it adds it to the single source of truth store by assigning the value to the name of the reducer function.

The second and most important helper function, called combineReducers will take an array of reducers, and use a closure to store that data.

It then returns a function that will loop over each of those reducers, this function accepts a state and an action, like a reducer.

It will then compare the current value of the state with no action with the state after an action has been performed.

If the state has changed, when compared to the current, it will return the updated store.

That is the bulk of the work done!

We just need to add in one more file that will make using our react hooks and context api state management system a little easier.

Create the last file in the store directory again, which we will call actions.js.

Add in these next few lines of code:

export const add = () => ({ type: 'INCREMENT' });
export const minus = () => ({ type: 'DECREMENT' });
export const modifyText = payload => ({ type: 'CHANGE_TEST_TEXT', payload });

These functions will be what we use to trigger a state change, they are simply presets of our actions stored in functions so we don’t need to type out our action object each time.

Now if you go back to your App.js, we will start using our new react hooks and context api state management system!

Open up App.js and make the following changes:

import React from 'react';
import { useStore } from './store/useStore';
import './App.css';
import { modifyText, add, minus } from './store/actions';

function App() {
  const { state, dispatch } = useStore();
  return (
    <div className="App">
      <header className="App-header">
        <input value={state.test.text} onChange={(e) => dispatch(modifyText(e.target.value))} />
        <p>
          {state.test.text}
        </p>

        <p>{state.count}</p>
        <button onClick={() => dispatch(add())}>add</button>
        <button onClick={() => dispatch(minus())}>minus</button>
      </header>
    </div>
  );
}

export default App;

Now run the react project so you can see it in your localhost, and have a play around with your react hooks and context api state management system!

The store will persist across the entire application, which is also accessible throughout the whole application by using the useStore hook.

All code is available on github if you want to look at the finished version https://github.com/WillMayger/atomized-objects-react-hooks-and-context-api-demo.

Will