State Container Components
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.
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
).
_25function 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!_8function 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:
- The state of the current pointer coordinates to show a hover line with tooltip
- A selection state to select data.
The pointer coordinates change often while the selection does not.
x: 0, y: 284
Here’s what the code looks like roughly.
_32const allData = { A: ..., B: ... }; // Imagine actual data in here_32_32function 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
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.
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.
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'!_42function 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_42export 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}
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.
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!
- Thinking in React in the React docs
- Application State Management with React by Kent C. Dodds.
- Using Composition in React to Avoid "Prop Drilling" by Micheal Jackson
-
↩Place code as close to where it’s relevant as possible
State Colocation will make your React app faster - Kent C. Dodds