State Container Components

Published Last updated v2.0.0

In React, state should be kept as close to where relevant as possible 1. Placing state unnecessarily high in the component tree can lead to , even more so when that state updates often.

When performance suffers, a component can be decomposed to isolate frequently updating state and by consequence, renders. That can mean moving some oft-changing state and the parts that need it down into a new component. It’s a way of colocating state and the UI elements that depend on it to make your React app faster.

Decomposing doesn’t exclusively mean moving down to a new, small leaf component, it comes in many shapes. Keep an eye open for unrelated states in a single component and try to find the fault lines between states and the JSX it ends up in, compose components accordingly.

are React components that hold some state to isolate it, such that rerenders are are avoided in parts of the UI that don’t need to know about said state.

If you can’t find a component where it makes sense to own the state, create a new component solely for holding the state and add it somewhere in the hierarchy above the common parent component.

Thinking in React: Identify where your state should

Let’s see what this means in practice. We got a <NavBar/> component with all the usual navbar stuff and an app-wide loading indicator, a spinner that pops up whenever something is being fetched (f.e. using react-query’s useIsFetching).


_25
function NavBar({ currentUser, sidebarOpen, toggleSidebar }) {
_25
const isFetching = useIsFetching();
_25
const [openMenu, setOpenMenu] = useState(null);
_25
_25
return (
_25
<nav>
_25
<SidebarButton sidebarOpen={sidebarOpen} onClick={toggleSidebar} />
_25
_25
<Logo />
_25
_25
{Boolean(isFetching) ? <Spinner /> : null}
_25
_25
<NotificationIcon onClick={() => setOpenMenu("notifications")}/>
_25
_25
{openMenu === "notifications" && <Notifications setOpenMenu={setOpenMenu} />}
_25
_25
<div onClick={() => setMenuOpen("user")}>
_25
Logged in as {currentUser.name}
_25
<Avatar image={currentUser.img} />
_25
</div>
_25
_25
{openMenu === "user" && <UserMenu setOpenMenu={setOpenMenu} />}
_25
</nav>
_25
);
_25
}

Note that the often changing isFetching state is not relevant to the other children of the navbar. It is, however, causing them to rerender everytime it’s toggled. Only <Spinner/> needs to know about isFetching.

To isolate the renders, make a new component and move <Spinner/> and isFetching down into it. Admittedly it’s very small, normally it wouldn’t be worth making a component for it. In this case, extracting it to a component is worth it because it holds the oft-changing isFetching state and therefore frequent rerenders in the <Navbar/> are avoided.


_8
// Component just to hold `isFetching` state, a State Container Component!
_8
function FetchingSpinner() {
_8
const isFetching = useIsFetching();
_8
_8
return Boolean(isFetching) ? (
_8
<Spinner />
_8
) : null;
_8
}

Let’s suppose we have a chart component with two states:

  1. The state of the current pointer coordinates to show a hover line with tooltip
  2. A selection state to select data.

The pointer coordinates change often while the selection does not.

x: 0, y: 284

0100200300400500600700050100150200250300350400A: 284

Here’s what the code looks like roughly.


_32
const allData = { A: ..., B: ... }; // Imagine actual data in here
_32
_32
function Chart() {
_32
const [selected, setSelected] = useState("A");
_32
const [pointerCoords, setPointerCoords] = useState({ x: 0, y: 0 });
_32
_32
const data = allData[selected];
_32
_32
const hoverValue = getHoverValue(data, pointerCoords);
_32
_32
return (
_32
<div>
_32
<Select selected={selected} setSelected={setSelected} />
_32
<svg
_32
onPointerMove={(event) => {
_32
// calculate chart coordinates here
_32
setPointerCoords({ x: point.x, y: point.y });
_32
}}
_32
>
_32
{/* `data` depends on `selected` */}
_32
<Axes data={data} />
_32
<Line data={data} />
_32
<Hover
_32
x={pointerCoords.x}
_32
y={pointerCoords.y}
_32
selected={selected}
_32
hoverValue={hoverValue}
_32
/>
_32
</svg>
_32
</div>
_32
);
_32
}

Half of these component do not need to know about the pointer coords but will still rerender whenever the pointer is moved. This .

Keep state as close to where it’s relevant as possible

Kent C. Dodds - Prop Drilling

Let’s break it down. <Axes /> and <Line /> depend on selected state, so does <Select /> which of course sets it. The function for onPointerMove on the <svg> sets the pointer coords state. At last, <Hover/> needs both states.

It helps to draw a quick diagram for these things, text surely isn’t the best way to explain it. Give it a quick try!

The problem here is that the pointer coords state is placed ‘too high’. <Hover/> needs both though, so we can’t move it down, right?

If you can’t find a component where it makes sense to own the state, create a new component solely for holding the state and add it somewhere in the hierarchy above the common parent component.

Thinking in React: Identify where your state should

We can in fact move it down! Simply move the components (the subtree) that needs the pointerCoords state into a new component that only holds that state; what I like to call a “State Container Component”.

We can make use of React’s composable nature to nest inside a state container component from the outside using the children prop. Move <svg> and its child <Hover/> to a separate component that holds the pointer coords state and wrap it around the components that don’t need it. Now the other components will not rerender when it is updated, it’s just a matter of good composition!


_42
// ChartSvg is a 'state container component'!
_42
function ChartSvg({ children, data, selected }) {
_42
const [pointerCoords, setPointerCoords] = useState({ x: 0, y: 0 });
_42
_42
const hoverValue = getHoverValue(data, pointerCoords);
_42
_42
return (
_42
<svg
_42
onPointerMove={(event) => {
_42
const point = ... // calculate chart coordinates here
_42
setPointerCoords({ x: point.x, y: point.y });
_42
}}>
_42
{children} {/* <Axes /> and <Line /> will be rendered here */}
_42
<Hover
_42
x={pointerCoords.x}
_42
y={pointerCoords.y}
_42
selected={selected}
_42
hoverValue={hoverValue}
_42
/>
_42
</svg>
_42
);
_42
}
_42
_42
export function Chart() {
_42
const [selected, setSelected] = useState("A");
_42
const data = rawData[selected];
_42
_42
... // chart things here
_42
_42
return (
_42
<div>
_42
<Select selected={selected} setSelected={setSelected} />
_42
<ChartSvg
_42
data={data}
_42
selected={selected}
_42
>
_42
<Axes data={data} />
_42
<Line data={data} />
_42
</ChartSvg>
_42
</div>
_42
);
_42
}

0100200300400500600700050100150200250300350400A: 284

And finally, here's a diagram that summarizes it. The green triangle represent the selected state and the blue square represents the pointerCoords state. I hope it clears things up a little.

Component hierarchy diagram showing before and after using a state container component for the previously given demo code

I recommend you to use the React dev tools with the "Highlight updates when components render." setting on and play around with both charts to see the differences. And finally, always measure before moving code in the name of performance!

  1. Place code as close to where it’s relevant as possible State Colocation will make your React app faster - Kent C. Dodds