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.
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.