Manage React state without redux

How to use React Context API in an easier and prettier way.

Lorenzo Spyna
ITNEXT

--

Disclaimer: I use Redux with React in production and I’m pretty happy with it.

TL;DR;

You suck! You just want the code but, if you’re in a hurry, download the repo or install the npm library in your project.

By the end of this article, you’ll learn how to read, write and remove data from one Component to another of your React app in a fast and easy way.

If your app storage is messy as this is, continue reading. Every article has a pic, so I needed to add this Photo by chuttersnap on Unsplash

Edit November 2019

I published a version of this story with React Hooks, which is more modern and updated. Follow this link to read it:

Intro

A few weeks ago I started a project with React and I didn’t want to use Redux because the project was a small one. If you are wondering the reason why I didn’t want to use redux, let’s have a look at its architecture and flows.

Redux

If you are familiar with redux, just skip this section, that a may sound a little boring for you.

Redux components

In redux you have to set up and maintain:

  • Provider: The <Provider /> makes the Redux store available to any nested components that have been wrapped in the connect() function.
  • connect: The connect() function connects a React component to a Redux store.
  • middlewares: Middlewares provide a third-party extension point between dispatching an action, and the moment it reaches the reducer.
  • actions: Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().
  • reducers: Reducers specify how the application’s state changes in response to actions sent to the store. Remember that actions only describe what happened, but don’t describe how the application’s state changes.

Not to mention that you have to create the redux Store with createStore, concatenate reducers with combineReducers, applyMiddlewares such as redux-thunk. Then you have to connect your Components with the state and the actions, using mapStateToProps, mapActionsToProps and sometimes mapDispatchToProps.

This means that if you decide to use redux in your project on Tuesday morning, you’ll pass the rest of the morning setting it up. If you have already used redux in another project, maybe you can save some time copying/pasting the code and start working just in a few hours.

Next, you have to decide which is the best place to put the business logic: Components? Actions? Reducers? Middlewares?

After three months, when you discover a bug in your app and re-open the codebase, you have to remember what did that reducer do and why the state isn’t changing as expected.

This was not to scare you, just my experience 😃.

What we want to do

The goal of this article is to find a way to reduce (pun intended) the complexity of our applications. The image below is a spoiler of the final architecture we want to achieve.

A more clean way to manage the app state

Back to the intro

After the above considerations, I came up with the decision of using the React Context API.

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

It accomplishes the goal of sharing a global state (or store) or whatever you wanna call it, but I find it very verbose and not really straightforward to use.

The goal of this article is to make the Global application state management easy to use (for developers). You will access the global store using store.set(’a_key’, 'a value’) in one Component and read that value using store.get(’a_key’) in another Component.

In other words, we are going to create a global key/value map to store data in, that re-renders your Components when its values change.

The first thing to do is to create a Context, Context.Provider and a Context.Consumer. The code is:

import React, { Component } from 'react'const StoreContext = React.createContext()class MyComponent extends Component {
static contextType = StoreContext
//this line does the magic, binding this.context to the value of the Provider
render() {
return <div>Hello {this.context.name}</div>
}
}
const MyApp = props => (
<div>
<MyComponent />
</div>
)
class AppWithContext extends Component {
state = { name: 'Spyna' }
render() {
return (
<StoreContext.Provider value={this.state}>
<MyApp />
</StoreContext.Provider>
)
}
}
export default AppWithContext
The working code on CodeSandbox

In this code snippet, you created a React Context then you wrapped MyApp Component into the context Provider and finally, you have bound MyComponent with the context using static contextType = StoreContext.

Since we set the value property of StoreContext.Provider to this.state (that is the state of AppWithContext), when something changes in that state, the AppWithContext will update, causing the re-render of its children.

To make these changes happen, you want some context (no pun intended) methods to: set, get, and remove data from the state. We used the AppWithContext state as the provider value, so we’ll to add these methods to it, that translated into code means:

class AppWithContext extends Component {
state = {
get: (key) => {
return this.state[key]
},
set: (key, value) => {
const state = this.state
state[key] = value
this.setState(state)
},
remove: key => {
const state = this.state
delete state[key]
this.setState(state)
}
}
...}

The provider value is this.state, so we added our methods there. You certainly noticed that the property name no longer exists in AppWithContext state, and we need to add it using the fresh, new and fabulous methods we’ve just created and added to the state. To test if this work, we’ll set the name property in MyComponent using the set method and we’ll read it in the render method using get.

class MyComponent extends Component {
static contextType = StoreContext
componentDidMount() {
let context = this.context
context.set('name', 'Spyna')
//sets the property 'name' with the value 'Spyna'
}

render() {
return <div>Hello {this.context.get('name')}</div>
//reads the property 'name' from the context
}
}
The working code on CodeSandbox

Yes, it works!

When the Component “did mount” we call context.set(’name’, ‘Spyna’) to set the property “name” in the context. Since the context is modified, the Component updates and when the render method is called, the value of “name” is printed. If you run this code you’ll see “Hello Spyna” printed on your screen.

This code doesn’t look pretty as I like. What we want to do is to make it a more declarative and easy to use. To achieve this goal we are going to create two High Order Component: one for the context Provider and the other one for the context Consumer.

According to React docs, a High Order Component is:

A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.

Concretely, a higher-order component is a function that takes a component and returns a new component.

const EnhancedComponent = higherOrderComponent(WrappedComponent);

If you want to know more about HOC have a look here.

Store Provider HOC

const createStore = WrappedComponent => {
return class extends React.Component {
state = {
get: key => {
return this.state[key]
},
set: (key, value) => {
const state = this.state
state[key] = value
this.setState(state)
},
remove: key => {
const state = this.state
delete state[key]
this.setState(state)
}
}
render() {
return (
<StoreContext.Provider value={this.state}>
<WrappedComponent {...this.props} />
</StoreContext.Provider>
)
}
}
}

If this code looks strange for you, have a look at the version without arrow functions =>: https://codesandbox.io/s/l21x87543z?autoresize=1&hidenavigation=1&module=%2Fsrc%2Fstore.js

The function createStore returns a Component that renders any Component passed as an argument inside the context Provider.

To use this HOC, all you have to do is just:

export default createStore(MyApp);

Store Consumer HOC

const withStore = WrappedComponent => {
return class extends React.Component {
render() {
return (
<StoreContext.Consumer>
{context => <WrappedComponent store={context} {...this.props} />}
<StoreContext.Consumer>
)
}
}
}

The function withStore returns a Component that renders any Component you passed as an argument inside the context Consumer. Plus it injects a prop called store into your Component, that is the context value.

Now you can access the context, doing:

const MyComponent = (props) => <div>{props.store.get('data')}</div>export default withStore(MyComponent)

Putting all together

Let’s create a file called store.js, add the two HOC and export them.

import React from 'react'const StoreContext = React.createContext()const createStore = WrappedComponent => {
return class extends React.Component {
state = {
get: key => {
return this.state[key]
},
set: (key, value) => {
const state = this.state
state[key] = value
this.setState(state)
},
remove: key => {
const state = this.state
delete state[key]
this.setState(state)
}
}
render() {
return (
<StoreContext.Provider value={this.state}>
<WrappedComponent {...this.props} />
</StoreContext.Provider>
)
}
}
}
const withStore = WrappedComponent => {
return class extends React.Component {
render() {
return (
<StoreContext.Consumer>
{context => <WrappedComponent store={context} {...this.props} />}
</StoreContext.Consumer>
)
}
}
}
export { createStore, withStore }

And finally, use the HOCs, with the below code, that looks much prettier than it looked in the beginning:

// MyComponent.js
import React, { Component } from 'react'
import { withStore} from './store';
class MyComponent extends Component {
componentDidMount() {
this.props.store.set('name', 'Spyna')
}
render() {
return <div>Hello {this.props.store.name}</div>
}
}
export default withStore(MyComponent)// App.js
import React, { Component } from 'react'
import { createStore} from './store'
import MyComponent from './MyComponent'
//import MyOtherWithStoreComponent from './MyOtherComponent'
const MyApp = props => (
<div>
<MyComponent />
{/*<MyOtherWithStoreComponent />*/}
</div>
)
export default createStore(MyApp)

Considerations

What you’ve done is just the beginning of a journey managing the state of your app. You may want to add some features to the store you already created. For example, you want to:

  • set an initial value, maybe reading it from the local storage or from an API: createStore(MyApp, initialValue).
  • get a default value if there are no data in the store: store.get('my_key', defaultValue).
  • provide immutability to the store methods, to avoid doing any damage by mistake, for example, if you call store.get = () => 'hey I just broke everything' you’ll break the get method.
  • isolate store methods from its values: now you can access a value from the store either doing store.get(’my_key’) or store.my_key. This is not bad at all, maybe you want to make this behavior configurable.
  • make the store methods returns a Promise so that you can use: store.remove(’my_key’).then(() =>{ doSomething() }).

All these ideas are great and you may think it takes a lot of time to implement them, that’s why I created the project react-store that already does all those stuff for you. The codebase is very small and if you want to copy it, have a look at the repo linked below.

As I stated at the beginning I’m a fan of Redux and think it is very useful, mostly when you deal with big projects. However, I find it a bit overkilling and uncomfortable to set up in small projects.

One thing I found working with this lib is that thing could turn a little messy unless you define some patterns or rules. An example will explain this better: our state (or store) could be modified from any Component in the project. This means that you could use store.set('logged_user', user) or store.get('logged_user') in multiple places (You can do this also with redux, it’s just a little bit more verbose). Let’s take this project structure as an example:

└── src
└── features
├── gallery
├── layout
│ ├── footer
│ └── header
├── login
├── navigation
│ ├── menu
│ └── sideNavigation
└── search

Each folder under features contains a feature of the project:

  • the header Component (that displays the user name) uses store.get('logged_user').
  • The side navigation Component (that changes the menu items when you log in/out) uses store.get('logged_user').
  • The login Component (that log the user in/out) uses store.set('logged_user').

The point is that you have to remember in each Component that you saved the logged user information in the store with the key ‘logged_user’.

The solution to this problem is to centralize that information and to do that you can create a storeMap.js file, where you put a mapping for each key you’re using within your store. This file will look as below:

export default {
LOGIN : {
LOGGED_USER : 'login/logged_user', //who is logged
ACCESS_TOKEN : 'login/access_token' //user token to call our API
},
SEARCH : {
SEARCH_TERM : 'search/search_term', //the term you searched
SEARCH_RESULTS : 'search/search_result', //search results
CURRENT_PAGE : 'search/current_page' // the current page
}
}

The Components will now use store.get(StoreMap.LOGIN.LOGGED_USER) instead of store.get('logged_user') which is:

  • less error-prone: you don’t have to write a string
  • centralized: the whole App state is mapped in a single file (that may be split into more files using import/export if needed)
  • easy to refactor: Just rename from your super IDE a key or a value.
  • self-explicative: When looking at this file you can have an idea of the whole application state
Complexity comparison between redux and react-store

--

--