How I dropped Redux for the Context API

React 16 introduced a new Context API to replace the deprecated one… Here is my way to replace Redux with this new API.

React 16 introduced a new Context API to replace the deprecated one. OK, it’s been more than a year since the release of version 16.3, but it still seems fresh in the React ecosystem.

This new API came with the promise to solve a lot of problems with the previous experimental way to use contexts. To me, it did a lot more; it changed the way I make React applications. This is the story of how I managed it.

I won’t give a course on how Redux works. If you want a refresher, you can check the amazing course from Dan Abramov on Egghead. Plus, you’ll eventually remove Redux from your apps, so do we need a full course on it?

There are a few requirements to understand the code: I will use React hooks and React fragments in the short form <>.

OK, let’s say we have an app that tells if I’m available for a beer. It consists of the following:

/* actions/beer.js */
export const TOGGLE_AVAILABILITY_FOR_BEER = 'TOGGLE_AVAILABILITY_FOR_BEER';
/* dispatchers/beer.js */
import {TOGGLE_AVAILABILITY_FOR_BEER} from '../actions/beer.js';
export const toggleBeerAvailability = () => ({
type: TOGGLE_AVAILABILITY_FOR_BEER
});
/* reducers/beer.js */
import {TOGGLE_AVAILABILITY_FOR_BEER} from '../actions/beer.js';
const beer = (state = {availableForBeer: true}, action) => {
switch (action.type) {
case TOGGLE_AVAILABILITY_FOR_BEER:
return {...state, availableForBeer: !state.availableForBeer};
default:
return state
}
};
export default beer;
/* components/beer.jsx */
import React from 'react';
import { connect } from 'react-redux';
import { toggleBeerAvailability } from '../dispatchers/beer';
const Beer = ({ isAvailable, toggleAvailability }) => (
<>
<div>
I'm currently
{isAvailable ? ' ' : ' not '}
available for a beer
</div>
<button onClick={toggleAvailability}>Change</button>
</>
);
const mapStateToProps = state => ({
isAvailable: state.availableForBeer
});
const mapDispatchToProps = dispatch => ({
toggleAvailability: () => dispatch(toggleBeerAvailability(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(Beer);

How to have a beer status in React with Redux.

In my sample code, I created four files to handle the parts of a Redux application:

  • actions/beer.js: A file that contains a constant for every action in my app. This could be inlined directly in the other files, but I like to keep things clear and concerns separated.
  • dispatchers/beer.js: The home of every action my Redux model has. In this case, I only have one toogleBeerAvailability method, which dispatches the action from the previous file.
  • reducers/beer.js: The storage engine of my Redux model, which changes the value of my availability if the TOGGLE_AVAILABILITY_FOR_BEER dispatcher is called.
  • components/beer.jsx: The component that shows and toggles my availability. We use react-redux to map the redux properties to my component props.

That’s a lot of code, but it’s necessary for a robust system with Redux. Now, we’re going to drop Redux with the same result. But first, why do we want to drop Redux?

I made that move simply to reduce weight in my application by removing two dependencies: redux and react-redux. I’m also not a big fan of having multiple dependencies in my applications, so I’m jumping on the possibility to remove two of them.

So here’s how it works. Keep in mind that it may not be a perfect solution or even a recommended one, but it’s the one I use in my projects and works. But let’s stop chatting and dive into the code.

I’m working with a single state file I call Provider. It contains everything to handle the state. In this first sample, it’s just a getter and a setter I receive from a state hook.

/* providers/BeerProvider.jsx */
import React, {createContext, useState} from 'react';
export const BeerContext = createContext();
const BeerProvider = (props) => {
const [isAvailable, setIsAvailable] = useState(true);
return (
<BeerContext.Provider {...props} value={{isAvailable, setIsAvailable}} />
);
}
export default BeerProvider;
/* components/Beer.jsx */
import React, {useContext} from 'react';
import { BeerContext } from '../providers/BeerProvider';
const Beer = ({ isAvailable }) => {
const {isAvailable, setIsAvailable} = useContext(BeerContext);
const toggleAvailability = () => setIsAvailable(!isAvailable);
return (
<>
<div>
I'm currently
{isAvailable ? ' ' : ' not '}
available for a beer
</div>
<button onClick={toggleAvailability}>Change</button>
</>
);
}
export default Beer;

How to have a beer status with the Context API

This looks much simpler and more efficient, but there are still a few issues to improve it:

  • The getters and setters are in the same object, which is a bit of a mess.
  • The toggleAvailability method is managed in the children component, which is not functional.
  • We will probably encounter performance issues due to our state change.

For the first one, I like to cut the object into two sub-objects, actions and values , like dispatchers and states in Redux. It eventually looks like this:

/* providers/BeerProvider.jsx */
import React, {createContext, useState} from 'react';
export const BeerContext = createContext();
const BeerProvider = (props) => {
const [isAvailable, setIsAvailable] = useState(true);
const value = {
actions: {setIsAvailable},
values: {isAvailable}
};
return (
<BeerContext.Provider {...props} value={value} />
);
}
export default BeerProvider;
/* components/Beer.jsx */
import React, {useContext} from 'react';
import { BeerContext } from '../providers/BeerProvider';
const Beer = ({ isAvailable }) => {
const {actions, values} = useContext(BeerContext);
const {setIsAvailable} = actions;
const {isAvailable} = values;
const toggleAvailability = () => setIsAvailable(!isAvailable);
return (
<>
<div>
I'm currently
{isAvailable ? ' ' : ' not '}
available for a beer
</div>
<button onClick={toggleAvailability}>Change</button>
</>
);
}
export default Beer;

How to have a beer status with the Context API and a bit of structure

For the second one, we just need to move the call into the parent component and add the action in our new actions section. It will make our Beer component a lot simpler.

/* providers/BeerProvider.jsx */
import React, {createContext, useState} from 'react';
export const BeerContext = createContext();
const BeerProvider = (props) => {
const [isAvailable, setIsAvailable] = useState(true);
const toggleAvailability = () => setIsAvailable(!isAvailable);
const value = {
actions: {setIsAvailable, toggleAvailability},
values: {isAvailable}
};
return (
<BeerContext.Provider {...props} value={value} />
);
}
export default BeerProvider;
/* components/Beer.jsx */
import React, {useContext} from 'react';
import { BeerContext } from '../providers/BeerProvider';
const Beer = ({ isAvailable }) => {
const {actions, values} = useContext(BeerContext);
return (
<>
<div>
I'm currently
{values.isAvailable ? ' ' : ' not '}
available for a beer
</div>
<button onClick={actions.toggleAvailability}>Change</button>
</>
);
}
export default Beer;

How to have a beer status with the Context API, structure, and consistency

As for performance, we still have two issues in our component:

  • The toggleAvailability method will be re-evaluated every time the Provider component is updated
  • The value object which contains the state will also be updated every time the Provider component has a change.

Fortunately, React provides two hooks to handle a cache of our data.

We will first encapsulate the toggleAvailability method in the useCallback hook. It will ensure the returned method will always be the same when the data in the second parameter has not changed. This will be possible because React’s useState hook guaranteed its set method would be the same despite the renders.

Then we’ll use the useMemo hook to encapsulate the value object. This hook is almost the same as useCallback but for objects. It will also get a second parameter to show what data it depends on.

/* providers/BeerProvider.jsx */
import React, {createContext, useCallback, useMemo, useState} from 'react';
export const BeerContext = createContext();
const BeerProvider = (props) => {
const [isAvailable, setIsAvailable] = useState(true);
const toggleAvailability = useCallback(() => setIsAvailable(!isAvailable), [isAvailable, setIsAvailable]);
const value = useMemo(() => {
actions: {setIsAvailable, toggleAvailability},
values: {isAvailable}
}, [isAvailable, setIsAvailable, toggleAvailability]);
return (
<BeerContext.Provider {...props} value={value} />
);
}
export default BeerProvider;
/* components/Beer.jsx */
import React, {useContext} from 'react';
import { BeerContext } from '../providers/BeerProvider';
const Beer = ({ isAvailable }) => {
const {actions, values} = useContext(BeerContext);
return (
<>
<div>
I'm currently
{values.isAvailable ? ' ' : ' not '}
available for a beer
</div>
<button onClick={actions.toggleAvailability}>Change</button>
</>
);
}
export default Beer;

And that’s all, folks! We no longer have Redux in our application and have a clean Context usage. I hope you give the Context API a try!

A new way for my React projects. Photo credit to me. A new way for my React projects. Photo credit to me.

References