React state management patterns in 2019

Tim Lind
5 min readMar 8, 2019

--

On Feb 6 2019 Dan Abramov announced the latest version of React v16.8 which now ships with production ready React Hooks. This includes new state management options for pure function components, with builtin reducer functionality.

To manage local state within a function component, React now offers two new options; useState() or useReducer(). I recently wrote a React proof of concept as a comparison between React and Angular for Luno.com, and got the chance to experiment with React’s new Hooks feature.

useState()

For more simple components useState() replaces what setState was used for with classes, except it doesn’t merge state attributes, so you may want to call it several times. React knows that it’s about to render this component again, so when the component calls useState() React can return the state it previously stored when rendering.

import React, { useState } from 'react';export default function Filter() {
var [searchTerm, setSearchTerm] = useState("")
function onChange(ev) {
setSearchTerm(ev.target.value)
}
return <input onChange={onChange} value={searchTerm}/>
}

useReducer()

For more complicated components / modules, React now has redux-like functionality out of the box.

import React, { useReducer } from 'react';export default function Filter(props) {
var [state, dispatch] = useReducer(reducer, initialState);
function onChange(ev) {
dispatch({ type: "change", searchTerm: ev.target.value })
}
return <input onChange={onChange} value={state.searchTerm}/>
}
export const initialState = {
searchTerm: ""
};
export function reducer(state, action) {
switch(action.type) {
case: "change":
return { ...state, searchTerm: action.searchTerm };
default:
return state;
}
}

You may be trying to compare this to redux by finding those trusty mapState and mapDispatch functions. This is where things get interesting, especially when it comes to those async actions creators you may be used to with redux thunk. While React doesn’t come equipped with any middleware functionality or async action creators, it turns out that you can achieve this with zero additional libraries or framework code. How you achieve this however, can either make or break the utility of your static analysis tools like your compiler, eslint, etc.

Currently, in the above example, the onChange inner function is a dispatcher (technically it’s a handler as well), and effectively this is where your redux mapDispatch function exists, as it has a binding to the above dispatch function returned from useReducer. This gives us the necessary static analysis when we pass it into the JSX. As your application grows larger however, you may want to separate these out of your component render function, especially for those async action creators that contain more complex logic.

Dispatchers and Selectors

The first change that you may want to make in comparison to Redux, is to have dispatchers instead of action creators, so that you can dispatch actions asynchronously instead of relying on a redux-thunk type middleware. By having dispatchers in a module you can keep complicated logic out of the render function and maintain static analysis within the render. The function within the render is effectively a handler function, a function which handles the event and forwards it to the dispatcher, it’s within the render function so that it has access to the dispatch() function which it passes over to the dispatcher.

The use of a selectors package is the same as redux’s mapState functions.

// Filter.jsimport * as dispatchers from './dispatchers';
import * as selectors from './selectors';
export default function Filter(props) {
var [state, dispatch] = useReducer(reducer, initialState);
function onChange(ev) {
dispatchers.change(dispatch, { searchTerm: ev.target.value })
}
return <input onChange={onChange}
value={selectors.searchTerm(state)}/>
}
// dispatchers.jsexport function change(dispatch, { searchTerm }) {
setTimeout(() => {
dispatch({ type: 'change', searchTerm })
}, 1000);
}
// selectors.jsexport function searchTerm(state) {
return state.searchTerm;
}

useStore()

As your component grows larger, you may want to separate out as much of the logic from your render function as possible, handlers, dispatchers, selectors etc. This is where you can use a pattern like useStore() below. Doing so will allow you to isolate the store for better testability. Unfortunately, doing this breaks static analysis slightly so you won’t see any warnings if a function you reference from the render i.e onChange={} doesn’t exist or is misspelled etc. This is where using a type checking system like Flow becomes extremely useful, as it will pick up these errors without any additional type declarations.

You’ll notice we’re passing in React.useReducer to useStore, this is so that we can inject our own useReducer for testing if we want to. Now these functions are all contained within the same module for easy reference, but decoupled enough to test well, you could still reference separate dispatcher and selector modules if desired.

// Filter.js
// @flow
import React from 'react';
import * as dispatchers from './dispatchers';
import * as selectors from './selectors';
import reducer from './reducer';
export default function Filter(props) {
var store = useStore(props, React.useReducer);
return <input onChange={store.handleChange}
value={store.selectSearchTerm()}/>
}
export function useStore(props, useReducer) {
var [state, dispatch] = useReducer(reducer);
var selectors = useSelectors(props, state);
var dispatchers = useDispatchers(dispatch, props, state);
var handlers = useHandlers(props, dispatchers, selectors);
return {
...selectors,
...dispatchers,
...handlers
};
}
export function useSelectors(props, state) {
return {
selectSearchTerm() {
return state.searchTerm;
}
};
}
export function useDispatchers(dispatch, props, state) {
return {
change({ searchTerm }) {
dispatch({ type: 'change', searchTerm });
}
};
}
export function useHandlers(props, dispatchers, selectors) {
return {
handleChange(ev) {
dispatchers.change({ searchTerm: ev.target.value });
}
};
}

new Store()

You may want to consider using a Store class for a more compact style. Although React’s new Hooks feature allows us to avoid creating class components, this doesn’t mean that we necessarily want to avoid using classes for our logic. In fact, being able to decouple the state management into it’s own class enables better testability and in my opinion more readability. Remember to bind the handlers to the class instance when defining them in the class.

// Filter.js
// @flow
import React from 'react';
export default function Filter(props) {
var store = new Store(props, React.useReducer);
return <input onChange={store.handleChange}
value={store.searchTerm}/>
}
export class Store {
constructor(props, useReducer) {
[this.state, this.dispatch] = useReducer(this.reducer);
}
get searchTerm() {
return this.state.searchTerm;
}
dispatchChange({ searchTerm }) {
this.dispatch({ type: 'change', searchTerm });
}
handleChange = (ev) => {
this.dispatchChange({ searchTerm: ev.target.value });
}
reducer(state, action) {
switch(action.type){
case 'change':
return { ...state, searchTerm };
case default:
return state;
}
}
}

Note that I haven’t used all these patterns in production, especially the class approach. For upcoming engineering articles subscribe to this publication or follow me on Twitter at IncrementalCode

--

--