Refactoring Redux using React Context

TL;DR: Context is more modular and doesn’t lock you into app architecture mistakes made early on. Also it’s less boilerplate!

Joshua Lacey
Jun 25, 2018 · 7 min read

The problem with Redux: Why import multiple dependencies into React and write tons of boilerplate in order to get React to do what it can already do natively? Why keep a global store for your entire application when much of its content is only used by few components?

imho I think Redux too often gets used where it doesn’t need to be. Don’t believe me? Go back into one of your redux apps and start refactoring. You’ll probably find that you didn’t need half the actions you thought you did.


This is a pretty contrived example because there is absolutely no reason why you should need redux to handle such a small application. Let’s use our imagination though and pretend we have a multipage application say for a survey and we need to keep all of the user’s input data in one state and pass that down.

The Redux Way…

For our purposes we’re just going to create couple inputs and then a checkbox and call it a day. Here’s how I’m gonna set up the file structure.

// File structure|_index.js
|_App.js
|_types.js
|_/ReduxForm/
|_index.js
|_actions.js
|_component.js
|_reducer.js

First set up your global redux store in index.js

// index.js < the root oneimport React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore } from 'redux';
import reducer from './reducer';
import { Provider } from 'react-redux';
const store = createStore(reducer);ReactDOM.render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('root'));

Our reducer is gonna look something like this:

// ReduxForm/reducer.jsimport * as types from '../types.js'const initialState = {
name: 'Joshua',
email: 'Joshua@lemonface.com',
over18: true
};
export default function app(state = initialState, action) {
switch (action.type) {
case types.NAME:
return { ...state, name: action.payload };
case types.EMAIL:
return { ...state, email: action.payload };
case types.OVER18:
return { ...state, over18: action.payload };
default:
return state;
}
}

Now let’s go ahead and create our form so we know what actions we’re gonna need:

import React from 'react';const Form = ({ name, email, over18, actions }) => (
<form>
<input
type="text"
onChange={actions.setName}
name="name"
value={name}
/>
<input
type="text"
onChange={actions.setEmail}
name="email"
value={email}
/>
<input
type="checkbox"
onChange={actions.toggleAge}
name="over18"
checked={over18}
/>
</form>
);
export default Form;

Some developers really like that this component can be totally free from any logic and state. I definitely agree that having state sprinkled around in two many components can cause unwanted side effects.

Now for those redux actions:

// ReduxForm/actions.jsimport * as types from '../types.js'export function setName(e) {
const { value } = e.target;
return { type: types.NAME, payload: value };
}
export function setEmail(e) {
const { value } = e.target;
return { type: types.EMAIL, payload: value };
}
export function toggleAge(e) {
const { checked } = e.target;
return { type: types.OVER18, payload: checked };
}

please ignore my code smell, I’m trying to make a point okay…

Now we gotta declare those constants in the types file.

// types.jsexport const NAME = 'NAME';export const EMAIL = 'EMAIL';export const OVER18 = 'OVER18';

This is where action types go to die and is also I think one of the biggest flaws in redux’s design. In a large application it is necessary to have many reducers in to separate out responsibilities. Redux has a method call combineReducers which will take all of the reducers in the application and… combine them into one so that you can access it from anywhere. The problem here is that it has to go through every case in your now massive switch statement in the reducer and check for a matching type. The problem arises when you have a reducer on the other side of the application that needs to do a really similar thing as another one. Then you end up with stuff like:

export const NAME_OF_OTHER_THING = 'NAME_OF_OTHER_THING';

Over time too when you refactor your reducers you’ll have all these constants that you don’t necessarily want to delete cause you don’t know what they are doing when in fact NAME isn’t used anymore and you’re only left with NAME_OF_OTHER_THING . Granted we probably should have been more specific in our naming in the first place, however I’m sure you could think of real world examples where you had to be overly specific and the obvious name never actually gets used.

Finally, lets look at the container component. This file seems to exist just to make sure that you have an ample amount of boilerplate in your application. It’s rarely more than 30 lines and your connected redux components just aren’t cool w/o it.

// ReduxForm/index.jsimport React from 'react';
import {connect} from 'react-redux';
import { bindActionCreators } from 'redux'
import * as actionCreators from './actions.js';
import component from './component.js'
function mapStateToProps(state) {
const {name, email, over18} = state;
return {
name,
email,
over18
};
}
function mapDispatchToProps (dispatch) {
return {actions: bindActionCreators(actionCreators, dispatch)};
}
export default connect(mapStateToProps, mapDispatchToProps)(component);

The Context Way

Unlike redux there aren’t a whole lot of developers writing about the context api as of yet. React Docs has a super thorough explanation and I definitely won’t give you a better source of info than you’ll find there. I mostly just wanted to rant about how much I don’t like redux since they’re all civil and constructive in their documentation.

Unlike Redux, Context works based on a parent component’s state.

I like this because:

  • It allows you to pass props to children which are deeply nested in the component tree just like redux,
  • It’s native to React so you don’t have to install other dependencies,
  • You have the freedom to create context out of any component’s state.
  • Oh and also less boilerplate!
// the three files you need-index.js
-context.js
-component.js

And yeah you don’t need to use Context for this use case, we’re still using our imagination here.

// context.jsimport React from 'react';export const {Provider, Consumer} = React.createContext({})

So the reason to make a separate file for these two lines of code is so that you can import Provider or Consumer from it. Another reason is if there are many contexts throughout the application you could instead set React.createContext({}) equal to a variable and then do something like myContext.Provider .

index.js will be our parent component who’s state we’re going to share with component.js.

// index.jsimport React, { Component } from 'react';
import { Provider } from './context';
import Form from './component';
class Store extends Component {
constructor() {
super()
this.state = {
actions: {
handleChange: this.handleChange,
toggleAge: this.toggleAge
}
name: 'Joshua',
email: 'Joshua@smileyface.com',
over18: true
};
handleChange = (e) => {
const { name, value } = e.target;
this.setState({ [name]: value });
};
toggleAge = (e) => {
const { checked } = e.target;
this.setState({ over18: checked });
}
render() {

return (
<Provider value={this.state} >
<Form />
</Provider>
);
}
}
export default Store;

What’s important to note here is that you are wrapping all child components in Provider and then passing in whatever value you want those children to be able to use. Here value is an object. This looks a lot closer to React’s native functionality of passing down props to children and is a major reason why I like it much more than redux.

Now for our form.

// component.jsimport React, { Component } from 'react';
import { Consumer } from './context';
const Form = () => (
<Consumer>
{value => (
<form>
<input
type="text"
onChange={value.actions.handleChange}
name="name"
value={value.name}
/>
<input
type="text"
onChange={value.actions.handleChange}
name="email"
value={value.email}
/>
<input
type="checkbox"
onChange={value.actions.toggleAge}
name="over18"
checked={value.over18}
/>
</form>
)}
</Consumer>
);
export default Form;

So here we just wrap our component’s children in Consumer which passes then a value as an argument.

Now lets clean context up a little bit:

We’re gonna create a wrapper for our component that we can use similarly to redux’s connect function.

In our context.js file let’s create a HOC wrapper like so:

import React from 'react';export const {Provider, Consumer} = React.createContext({});export function contextWrapper(WrappedComponent) {
return function Wrapper(props) {
return(
<Consumer>
{ value => (
<WrappedComponent {...props} {...value} />
)}
</Consumer>
)
}
}

Now in our component.js :

import React, { Component } from 'react';
import { contextWrapper } from './context';
const Form = ({actions, email, over18, name}) => (
<form>
<input
type="text"
onChange={actions.handleChange}
name="name"
value={name}
/>
<input
type="text"
onChange={actions.handleChange}
name="email"
value={email}
/>
<input
type="checkbox"
onChange={actions.toggleAge}
name="over18"
checked={over18}
value={over18}
/>
</form>
);
export default contextWrapper(Form);

Caveats:

  • With the structure I listed above, any component under the Provider will be re-rendered when the parent state changes. Therefore it’s probably not the best thing to use if you’re trying to maintain a global state like you would with redux.
  • There’s another one listed on the Context API Docs that talks about making sure that you pass an object that is straight from the application’s state instead of instantiating an object in the render and then passing it to the value.

I think that since that there is a well established design pattern for Redux it makes developers comfortable using it in a team setting. There are so many blog post about using redux that surely everyone else on the team will be on the same page when it comes to best practice. I’ve seen a few cases though where developers get so excited about Redux that they wire up every small component using the index, component, actions, reducer pattern before ever really needing it. I worked with this one developer that was so allergic to component level state that even a simple toggle action for him had to be woven through redux (update: he uses component state for toggles now).

In conclusion, I still use and like redux in the right project. There are use cases for redux that context doesn’t answer. Context is a great addition and I hope that React developers can be quicker to adopt it where it makes sense. Though maybe React Hooks is gonna take the crown there…

continued reading:

the ugly side of redux

Joshua Lacey

Written by

Fullstack Webdeveloper: Javascript, Node.js, React.js, Ruby on Rails

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade