Storing State in React Applications

React is not prescriptive of where and how to store state. To some, this is welcomed flexibility, to others this is painful confusion. Let's clear the air. 26 March 2020

# Background

When building React apps, the question of where to store state often gets raised. Should it be stored in the component? Is React Context the new way to store state? Is Redux dead ? By considering locality, we can select the best way to store state.

# Local state

In many cases, state only needs to be local to a component. So, storing it within the component itself is sensible. We can have as many simultaneous instances of such a component, and each will have its own independent state.

function Example () {
const [foo, setFoo] = useState()
const toggleFoo = (s) => setFoo(!s)

return (
<button onClick={toggleFoo}>
{foo ? "truthy" : "falsey"}
</button>
)
}

# Distributed state

But sometimes, state needs to span several nested React components (not necessarily the same as nested DOM elements). To share such state, we need to use React's Context API.

For example, we might want to generate both a HTML and plain text email from the same template. Below, Email provides an EmailType context to its descendents. Any descendent Header or Paragraph can then render the appropriate output for the given context.

import { createContext, useContext } from React

const EmailType = createContext()

function Email ({ children, type }) {
return (
<EmailType.Provider value={type}>
{children}
</EmailType.Provider>
)
}

function Header ({ children }) {
const type = useContext(EmailType)
return type === "html"
? <h1>{children}</h1>
: `# ${children.toUpperCase()}`
}

function Paragraph ({ children }) {
const type = useContext(EmailType)
return type === "html"
? <p>{children}</p>
: children
}

function Welcome ({ type }) {
return (
<Email type={type}>
<Header>Hello</Header>
<Paragraph>Welcome to my site</Paragraph>
</Email>
)
}

function emailBody (type) {
return ReactDOMServer.renderToString(<Welcome type={type} />)
}

sendEmail({
html: emailBody("html"),
txt: emailBody("txt")
})

Much like Javascript variable scopes, each context provider creates its own scope. Parallel independent contexts may exist, even nested contexts may exist. In the latter, inner and more specific contexts take precedence. Or equivalently, that it shadows the outer context. For example, below is a group of related components that share common state which altogether produces the following HTML.

<p>one:/a/b/c</p>
<p>two:/x/y/z</p>

RootLayer provides a NameContext that it shares with its descendants. Within each provider, Layer expects to be able to gain access to the NameContext, and then republish a different value to its descendents.

import { createContext, useContext } from "react"

const NameContext = createContext()

function RootLayer ({ children, prefix }) {
return (
<NameContext.Provider value={`${prefix}:`}>
{children}
</NameContext.Provider>
)
}

function Layer ({ children, name }) {
const parentName = useContext(NameContext)
const childName = `${parentName}/${name}`

return (
<NameContext.Provider value={childName}>
{children || <p>{childName}</p>}
</NameContext.Provider>
)
}

function Example () {
return (
<>
<RootLayer prefix="one">
<Layer name="a">
<Layer name="b">
<Layer name="c" />
</Layer>
</Layer>
</RootLayer>
<RootLayer prefix="two">
<Layer name="x">
<Layer name="y">
<Layer name="z" />
</Layer>
</Layer>
</RootLayer>
</>
)
}

# Out in the wild

This pattern of using multiple related components as one unit, is actually quite common in the React community.

For example, formik  uses this strategy to allow creation of forms, which naturally has distributed state. The Formik component creates a new context which holds the form field values and various error states, which descendents Form, Field, and ErrorMessage make use. (The syntax may look unusual, but it is just render props ).

import { Formik, Form, Field, ErrorMessage } from "formik"

function ExampleForm () {
return (
<Formik initialValues={initialValues} validate={validate} onSubmit={onSubmit}>
{({ isSubmitting }) => (
<Form>
<Field type="email" name="email" />
<ErrorMessage name="email" component="div" />
<Field type="password" name="password" />
<ErrorMessage name="password" component="div" />
<button type="submit" disabled={isSubmitting}>Submit</button>
</Form>
)}
</Formik>
)
}

react-router  also make use of this strategy to change components based on routes. The Router component creates a context which holds the location and history states, that Switch, Route, and Link can access.

import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"

function ExampleRouter () {
return (
<Router>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
<Switch>
<Route exact path="/"><Home /></Route>
<Route path="/about"><About /></Route>
</Switch>
</Router>
)
}

Even styled-components  has a ThemeProvider which provides a context for styling, which the styled function uses to create Higher Order Components .

import styled, { ThemeProvider } from "styled-components"

const Button = styled.button`
color:
${props => props.theme.color};
background:
${props => props.theme.background};
`


function ExampleStyles () {
return (
<ThemeProvider theme={theme}>
<Button>click here</Button>
</ThemeProvider>
)
}

# Beyond the component lifecycle

One limitation with storing state in a React component; either locally within a component or with a context provider, is that the state can be destroyed whenever the shape of the React tree changes, and the stateful component or provider is unmounted. Further, another limitation is that access to a context is restricted to the children of the provider.

To solve these limitations, state needs to live outside the React tree, using state management libraries like redux  and mobx . With such an architecture, state can outlive the React component lifecycle, and has reach for beyond that of the provider's nested children.

Subsequently, with state living outside the React tree, it becomes possible to share that state with other non-DOM libraries, say one that updates a canvas (eg. Mapbox ) or one that syncs data with a server (eg. FeathersJS ).

In this example below, a common Redux store is shared between React and some arbitrary non-React code. For the non-React code, we use low-level store.subscribe() and store.dispatch() methods, while for the React code, we use react-redux , which is a convenience wrapper that performs much the same function (and more).

import { createStore } from "redux"
import { Provider } from "react-redux"

const store = createStore(reducers, {})

ReactDOM.render(
<Provider store={store}>
<RootComponent />
</Provider>,
element
)

store.subscribe(nonReact.handleChanges)
nonReact.on("someEvent", (data) => {
store.dispatch(makeChanges(data))
})

Storing state at such a high level can sometimes result in performance issues, as every state change results in all of its listeners being triggered. Thus smarts to decide whether to update and render may be necessary. For example, Redux Form  stores form data in Redux, which means the entire React tree is re-evaluated on every keystroke potentially leading to poor performance if not handled properly.

# Summary

Deciding where to store state in a React application is full of traps. For highly local use, storing it in a component is performant and convenient. For medium locality, and especially for sharing across multiple nested components, the Context API provides the best option. For distributed use, spanning component lifecycles, or even different listeners altogether, a dedicated state management store is ideal, but care is needed to ensure that performance does not suffer.