You might have come across the term "state management" while learning about React. But what does it really mean?

In this post, I will introduce you to state management in React and how you can get started with it.

Hand drawn sketch of the word state inside a cogwheel

Why do we need state management?

I think the best way to introduce state management is to describe why we need it in the first place.

State is a core concept in React. Instead of explicitly telling the browser to render certain elements, with React, we express how elements should render for a given state. Then we set or change the state and React takes care of instructing the browser to render the correct elements.

For example, here is a simple counter implemented using plain Javascript.

<div>
<h1>Counter</h1>
<p>
Count:
<span id="count">0</span>
</p>
<button onclick="increment()">Add one</button>
<script>
function increment() {
const countElement = document.getElementById('count');
countElement.innerText = Number(countElement.innerText) + 1;
}
</script>
</div>

Here is the same counter implemented using React.

import { useState } from 'react';

const Counter = () => {
const [count, setCount] = useState(0);

const increment = () => {
setCount(count => count + 1);
};

return (
<div>
<h1>Counter</h1>
<p>Count: {count}</p>
<button onClick={increment}>Add one</button>
</div>
);
}

Notice that with React we are not telling the browser to change the value in the paragraph (p) element. We just change the state and React takes care of the rest.

Working with state in a single component like in the example above is quite straightforward. However, things get messy when you need to share state across several components.

React's built-in way to share state across several components is to pass the state value as a prop as shown below.

import { useState } from 'react';

const CounterDisplay = ({ count }) => (
<p>Count: {count}</p>
)

const Counter = () => {
const [count, setCount] = useState(0);

const increment = () => {
setCount(count => count + 1);
};

return (
<div>
<h1>Counter</h1>
{/* Pass the count state value as a prop to the CounterDisplay component */}
<CounterDisplay count={count} />
<button onClick={increment}>Add one</button>
</div>
);
}

But what if you need to share state with a component that is several layers below? You will have to pass the value between each component until it gets to where it needs to go.

import { useState } from 'react';

const CounterDisplay = ({ count }) => (
<p>Count: {count}</p>
)

const RedCounterDisplay = ({ count }) => {
<div style={{ color: 'red' }}>
{/* RedCounterDisplay doesn't use the count value. It just passes it on to CounterDisplay */}
<CounterDisplay count={count} />
</div>
}

const Counter = () => {
const [count, setCount] = useState(0);

const increment = () => {
setCount(count => count + 1);
};

return (
<div>
<h1>Counter</h1>
<RedCounterDisplay count={count} />
<button onClick={increment}>Add one</button>
</div>
);
}

The React community calls this prop drilling. In larger React apps, this leads to bloated components that accept dozens of props only to pass them on to child components.

One way to avoid prop drilling is to use React context.

import { useState, useContext, createContext } from 'react';

const CountContext = createContext(0);

const CounterDisplay = () => {
// count value is taken out of the context
const count = useContext(CountContext);
return (
<p>Count: {count}</p>
)
}

const RedCounterDisplay = () => {
<div style={{ color: 'red' }}>
{/* count is not passed manually. RedCounterDisplay doesn't even know about it. */}
<CounterDisplay />
</div>
}

const Counter = () => {
const [count, setCount] = useState(0);

const increment = () => {
setCount(count => count + 1);
};

return (
<div>
<h1>Counter</h1>
<CountContext.Provider value={count}>
<RedCounterDisplay />
</CountContext.Provider>
<button onClick={increment}>Add one</button>
</div>
);
}

But React Context isn't designed for this use case. When you use React Context to handle state, you run into performance issues due to the way it behaves.

This is where state management libraries come in. They provide a way to easily share state across several components in a performant manner. They also provide a pattern for organizing how you access and update state so that it is easier to manage as the app grows in size.

So state management is the process of organizing your app to handle state in a maintainable and performant way.

Getting started with state management

All React apps can benefit from better state management. The most straightforward way to do this is to use an existing library and adopt the patterns that it provides. But depending on your circumstances, there are different ways in which you can proceed.

If you are working on a React app that doesn't use a state management or data-fetching library, you can definitely improve your development experience by using them. Some options are to use Tanstack query alongside Zustand or to use Redux toolkit.

If you are already using a data fetching library, you are likely to have only a small amount of global state to manage manually. While you could use a React hook (either useState or useReducer) and React context for this, I still think that adopting a simple state management library like [Zustand]((https://docs.pmnd.rs/zustand/getting-started/introduction) could lead to a better development experience and performance.

To learn more about the approaches mentioned above, you can check out this post where I go over them in more detail.

Prabashwara Seneviratne (bash)

Written by

Prabashwara Seneviratne (bash)

Lead frontend developer