This site runs best with JavaScript enabled.

Application State Management with React

Software Engineer, React Training, Testing JavaScript Training

Photo by Rene Böhmer


How React is all you need to manage your application state

Managing state is arguably the hardest part of any application. It's why there are so many state management libraries available and more coming around every day (and even some built on top of others... There are hundreds of "easier redux" abstractions on npm). Despite the fact that state management is a hard problem, I would suggest that one of the things that makes it so difficult is that we often over-engineer our solution to the problem.

There's one state management solution that I've personally tried to implement for as long as I've been using React, and with the release of React hooks (and massive improvements to React context) this method of state management has been drastically simplified.

We often talk about React components as lego building blocks to build our applications, and I think that when people hear this, they somehow think this excludes the state aspect. The "secret" behind my personal solution to the state management problem is to think of how your application's state maps to the application's tree structure.

One of the reasons redux was so successful was the fact that react-redux solved the prop drilling problem. The fact that you could share data across different parts of your tree by simply passing your component into some magical connect function was wonderful. Its use of reducers/action creators/etc. is great too, but I'm convinced that the ubiquity of redux is because it solved the prop drilling pain point for developers.

This is the reason that I only ever used redux on one project: I consistently see developers putting all of their state into redux. Not just global application state, but local state as well. This leads to a lot of problems, not the least of which is that when you're maintaining any state interaction, it involves interacting with reducers, action creators/types, and dispatch calls, which ultimately results in having to open many files and trace through the code in your head to figure out what's happening and what impact it has on the rest of the codebase.

To be clear, this is fine for state that is truly global, but for simple state (like whether a modal is open or form input value state) this is a big problem. To make matters worse, it doesn't scale very well. The larger your application gets, the harder this problem becomes. Sure you can hook up different reducers to manage different parts of your application, but the indirection of going through all these action creators and reducers is not optimal.

Having all your application state in a single object can also lead to other problems, even if you're not using Redux. When a React <Context.Provider> gets a new value, all the components that consume that value are updated and have to render, even if it's a function component that only cares about part of the data. That might lead to potential performance issues. (React-Redux v6 also tried to use this approach until they realized it wouldn't work right with hooks, which forced them to use a different approach with v7 to solve these issues.) But my point is that you don't have this problem if you have your state more logically separated and located in the react tree closer to where it matters.


Here's the real kicker, if you're building an application with React, you already have a state management library installed in your application. You don't even need to npm install (or yarn add) it. It costs no extra bytes for your users, it integrates with all React packages on npm, and it's already well documented by the React team. It's React itself.

React is a state management library

When you build a React application, you're assembling a bunch of components to make a tree of components starting at your <App /> and ending at your <input />s, <div />s and <button />s. You don't manage all of the low-level composite components that your application renders in one central location. Instead, you let each individual component manage that and it ends up being a really effective way to build your UI. You can do this with your state as well, and it's very likely that you do today:

1function Counter() {
2 const [count, setCount] = React.useState(0)
3 const increment = () => setCount(c => c + 1)
4 return <button onClick={increment}>{count}</button>
5}
6
7function App() {
8 return <Counter />
9}

Edit React Codesandbox

Note that everything I'm talking about here works with class components as well. Hooks just make things a bit easier (especially context which we'll get into in a minute).

1class Counter extends React.Component {
2 state = {count: 0}
3 increment = () => this.setState(({count}) => ({count: count + 1}))
4 render() {
5 return <button onClick={this.increment}>{this.state.count}</button>
6 }
7}

"Ok, Kent, sure having a single element of state managed in a single component is easy, but what do you do when I need to share that state across components? For example, what if I wanted to do this:"

1function CountDisplay() {
2 // where does `count` come from?
3 return <div>The current counter count is {count}</div>
4}
5
6function App() {
7 return (
8 <div>
9 <CountDisplay />
10 <Counter />
11 </div>
12 )
13}

"The count is managed inside <Counter />, now I need a state management library to access that count value from the <CountDisplay /> and update it in <Counter />!"

The answer to this problem is as old as React itself (older?) and has been in the docs for as long as I can remember: Lifting State Up

"Lifting State Up" is legitimately the answer to the state management problem in React and it's a rock solid one. Here's how you apply it to this situation:

1function Counter({count, onIncrementClick}) {
2 return <button onClick={onIncrementClick}>{count}</button>
3}
4
5function CountDisplay({count}) {
6 return <div>The current counter count is {count}</div>
7}
8
9function App() {
10 const [count, setCount] = React.useState(0)
11 const increment = () => setCount(c => c + 1)
12 return (
13 <div>
14 <CountDisplay count={count} />
15 <Counter count={count} onIncrementClick={increment} />
16 </div>
17 )
18}

Edit React Codesandbox

We've just changed who's responsible for our state and it's really straightforward. And we could keep lifting state all the way to the top of our app.

"Sure Kent, ok, but what about the prop drilling problem?"

This is one problem that's actually also had a "solution" for a long time, but only recently was that solution "official" and "blessed." As I said, many people reached for react-redux because it solved this problem using the mechanism I'm referring to without them having to be worried about the warning that was in the React docs. But now that context is an officially supported part of the React API, we can use this directly without any problem:

1// src/count/count-context.js
2import React from 'react'
3
4const CountContext = React.createContext()
5
6function useCount() {
7 const context = React.useContext(CountContext)
8 if (!context) {
9 throw new Error(`useCount must be used within a CountProvider`)
10 }
11 return context
12}
13
14function CountProvider(props) {
15 const [count, setCount] = React.useState(0)
16 const value = React.useMemo(() => [count, setCount], [count])
17 return <CountContext.Provider value={value} {...props} />
18}
19
20export {CountProvider, useCount}
21
22// src/count/page.js
23import React from 'react'
24import {CountProvider, useCount} from './count-context'
25
26function Counter() {
27 const [count, setCount] = useCount()
28 const increment = () => setCount(c => c + 1)
29 return <button onClick={increment}>{count}</button>
30}
31
32function CountDisplay() {
33 const [count] = useCount()
34 return <div>The current counter count is {count}</div>
35}
36
37function CountPage() {
38 return (
39 <div>
40 <CountProvider>
41 <CountDisplay />
42 <Counter />
43 </CountProvider>
44 </div>
45 )
46}

Edit React Codesandbox

NOTE: That particular code example is VERY contrived and I would NOT recommend you reach for context to solve this specific scenario. Please read Prop Drilling to get a better sense for why prop drilling isn't necessarily a problem and is often desirable. Don't reach for context too soon!

And what's cool about this approach is that we could put all the logic for common ways to update the state in our useContext hook:

1function useCount() {
2 const context = React.useContext(CountContext)
3 if (!context) {
4 throw new Error(`useCount must be used within a CountProvider`)
5 }
6 const [count, setCount] = context
7
8 const increment = () => setCount(c => c + 1)
9 return {
10 count,
11 setCount,
12 increment,
13 }
14}

Edit React Codesandbox

And you could easily change this to useReducer rather than useState as well:

1function countReducer(state, action) {
2 switch (action.type) {
3 case 'INCREMENT': {
4 return {count: state.count + 1}
5 }
6 default: {
7 throw new Error(`Unsupported action type: ${action.type}`)
8 }
9 }
10}
11
12function CountProvider(props) {
13 const [state, dispatch] = React.useReducer(countReducer, {count: 0})
14 const value = React.useMemo(() => [state, dispatch], [state])
15 return <CountContext.Provider value={value} {...props} />
16}
17
18function useCount() {
19 const context = React.useContext(CountContext)
20 if (!context) {
21 throw new Error(`useCount must be used within a CountProvider`)
22 }
23 const [state, dispatch] = context
24
25 const increment = () => dispatch({type: 'INCREMENT'})
26 return {
27 state,
28 dispatch,
29 increment,
30 }
31}

Edit React Codesandbox

This gives you an immense amount of flexibility and reduces complexity by orders of magnitude. Here are a few important things to remember when doing things this way:

  1. Not everything in your application needs to be in a single state object. Keep things logically separated (user settings does not necessarily have to be in the same context as notifications). You will have multiple providers with this approach.
  2. Not all of your context needs to be globally accessible! Keep state as close to where it's needed as possible.

More on that second point. Your app tree could look something like this:

1function App() {
2 return (
3 <ThemeProvider>
4 <AuthenticationProvider>
5 <Router>
6 <Home path="/" />
7 <About path="/about" />
8 <UserPage path="/:userId" />
9 <UserSettings path="/settings" />
10 <Notifications path="/notifications" />
11 </Router>
12 </AuthenticationProvider>
13 </ThemeProvider>
14 )
15}
16
17function Notifications() {
18 return (
19 <NotificationsProvider>
20 <NotificationsTab />
21 <NotificationsTypeList />
22 <NotificationsList />
23 </NotificationsProvider>
24 )
25}
26
27function UserPage({username}) {
28 return (
29 <UserProvider username={username}>
30 <UserInfo />
31 <UserNav />
32 <UserActivity />
33 </UserProvider>
34 )
35}
36
37function UserSettings() {
38 // this would be the associated hook for the AuthenticationProvider
39 const {user} = useAuthenticatedUser()
40}

Notice that each page can have its own provider that has data necessary for the components underneath it. Code splitting "just works" for this stuff as well. How you get data into each provider is up to the hooks those providers use and how you retrieve data in your application, but you know just where to start looking to find out how that works (in the provider).

For even more about why this colocation is beneficial, check out my "Colocation" blog post. And for more about context, read How to use React Context effectively

Conclusion

Again, this is something that you can do with class components (you don't have to use hooks). Hooks make this much easier, but you could implement this philosophy with React 15 no problem. Keep state as local as possible and use context only when prop drilling really becomes a problem. Doing things this way will make it easier for you to maintain state interactions.

Discuss on TwitterEdit post on GitHub

Share article
loading relevant upcoming workshops...
Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.