React Context API & Redux - Which and Why?

2020, Jul 29    

On a recent client project, I was looking to take an existing web application that had been written with vanilla JS and and implement a version of it that used React.

The rationale for this decision being that the existing app had become too complex and brittle, which meant integrating any changes or new features was cumbersome and time consuming.

A “framework” approach would alleviate these problems, and also create some reusable bits that could be used across other projects.

The first thing I looked at was state management, as the current solution had nothing. My previous experience is with Redux, and I really do love it, but this time the native context api was mentioned as an alternative.

This was pretty new to me. I’d known what it was, but up until recently the react documentation had said not to use it  - but a quick glance over some articles had praised its usage, and even suggested that it was time to move away from Redux.

Using an “easy” built in part of the framework also appealed as it could potentially shorten the learning curve for the existing team, and start them off on the right foot with the latest and greatest.

What follows are a few notes and a sample I put together as I myself began to explore the context api, and then I’ll talk about why I decided not to use it and proceed with Redux.


Working Demo

I have a complete, working demo on my github page here. If you want to skip the blurb, go ahead and pull it down, run npm install and you will see the create-react-app landing page (which I have used to scaffold the sample) with my name, which is the data that is saved to and then loaded from the store.

If you open up the App.js file, you will see a StoreProvider element that is wrapped around the App element which has been imported from /context/store.

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

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

This is the entry point for the application, and this “StoreProvider” is assessible to our app.

If you open the “src” folder, and then expand the “context”, this is where the meat of the code lives

content of the Context folder in the sample code

if you are familiar with Redux, the nameActions and nameReducer should look familiar, and the constants file just exports a single action name string (UPDATE_NAME) to prevent accidental typos. I’m going to leave these alone for now, and just focus on store.js - let’s see what’s happening in there

import React, { createContext, useContext, useReducer } from 'react';
import nameReducer from './reducers/nameReducer';

const StoreContext = createContext();

const initialState = {
  name: '',
};

const combineReducers = (...reducers) => (state = initialState, action = {}) => {
    for (let i = 0; i < reducers.length; i++)
        state = reducers[i](state, action);
    return state;
};

export const StoreProvider = ({ children }) => {
  const [state, dispatch] = useReducer(combineReducers(nameReducer), initialState);

  return (
    <StoreContext.Provider value={{state, dispatch}}>
      {children}
    </StoreContext.Provider>
  )
}

export const useStore = () => useContext(StoreContext);

Here we are importing React and the react hooks that this provider is using, before setting up our StoreContext object, defining the initial state, and defining a helper method that I wrote to combine any reducers (this is a bit redundant for this sample, as we only have 1 reducer) and then using all of that to define our StoreProvider.

There are a few bits of magic here. In out StoreProvider, we are creating a state object using the useReducer hook. This hook is the glue that allows our provider (and then eventually a component) to use the reducer functions from the state management.

The createContext method (imported from React) is another bit of magic, and will create a Context object, and when “React renders a component that subscribes to this Context object, it will read the current context value from the closest matching Provider above it in the tree” - according to the React documentation.

The last bit of magic (Wishes come in 3s, right?) you can see in our StoreProvider constant, in that we are returning that provider, with it’s value set to the state we just configured with the useReducer hook.

finally, we are exporting our own useStore hook, which we will see a component use in just a moment to access the store.

This is a very simple sample which has just a single reducer which has 1 value (name) saved in the store and is available to the full application. Using this pattern it should be very easy to create multiple contexts to exposes slices of the store to different levels of your application.

Let’s see how this is actually being used by a component - if you take a look at App.js you will see this

import React, {useEffect, useCallback} from 'react';
import logo from './logo.svg';
import './App.css';
import {useStore} from "./context/store";
import {updateNameAction} from './context/actions/nameActions';

function App() {

  const {state, dispatch} = useStore();
  const stableDispatch = useCallback(dispatch, [])

    useEffect(() =>{
        stableDispatch(updateNameAction("kris"))
   },[stableDispatch])

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          <p>Name from store: {state.name}</p>
        </a>
      </header>
    </div>
  );
}

export default App;

Inside our App, we have imported and used our useStore hook to get a reference to the store, and in our useEffect hook (which triggers when the component is rendered) we are dispatching our updateNameAction which updates the name in the store to “Kris”, which in turn is rendered as part of the <P> tag under the react logo.

Pros, Cons & Why I didnt use this as part of this project

This is a really fantastic, simple and easy to maintain store to use as part of an app.

If I was building a basic app, that was only managing some basic state - such as theming or some basic component state, then I would definitely give this a go.

Where this started to break down for me when I tried to push it in a direction of doing anything complex.

Like many modern web apps, the project required fetching data from other sources with a typical CRUD pattern. Using redux, there are third party middleware that does this  - Thunk & Sagas come to mind.

The context API does not support middleware out of the box. Redux does - and it does it really well.

During my research and playing around, I really did try to get asynchronous fetches working in a clean way, but I was never happy with the result. I always ended up having to manually write Promise handler code, and this got even more messy when multiple calls were needed in the same action.

I also got into a bit of a muddle with handing dispatch. there is a useDispatch hook but I kept getting different results and managing concurrency was a bit of an issue. Some of my experimentation took a dependency on Redux (bindActionCreators) and that kind of goes against the point of trying to not use Redux :)

In the end, I decided to just stick with Redux - it just works. I have no doubt that the context api can be made to do the things I wanted it to do, but as always with client projects time is of the essence and Redux is still the more mature platform of the two.