State Container Components

Published

This was the example I initially wrote for the 'State Container Components' trick in Neat Little React Tricks but it wasn't exactly 'little'. It's a good example nonetheless so it get's it own post.

State Container Components are components that just 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. It's a way of colocating state to make your React app faster.

Let's say we have a chart component with two states, 1) the state of the current pointer coordinates to show a hover line with tooltip and 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.

const allData = { A: ..., B: ... }; // Imagine actual data in here

function Chart() {
  const [selected, setSelected] = useState("A");
  const [pointerCoords, setPointerCoords] = useState({ x: 0, y: 0 });

  const data = allData[selected];

  const hoverValue = getHoverValue(data, pointerCoords);

  return (
    <div>
      <Select selected={selected} setSelected={setSelected} />
      <svg
        onPointerMove={(event) => {
          // calculate chart coordinates here
          setPointerCoords({ x: point.x, y: point.y });
        }}
      >
        {/* `data` depends on `selected` */}
        <Axes data={data} />
        <Line data={data} />
        <Hover
          x={pointerCoords.x}
          y={pointerCoords.y}
          selected={selected}
          hoverValue={hoverValue}
        />
      </svg>
    </div>
  );
}

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.

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!

// ChartSVG is a 'state container component'!
function ChartSVG({ children, data, selected }) {
  const [pointerCoords, setPointerCoords] = useState({ x: 0, y: 0 });

  const hoverValue = getHoverValue(data, pointerCoords);

  return (
    <svg
      onPointerMove={(event) => {
        const point = ... // calculate chart coordinates here
        setPointerCoords({ x: point.x, y: point.y });
     }}>
      {children} {/* <Axes /> and <Line /> will be rendered here */}
      <Hover
        x={pointerCoords.x}
        y={pointerCoords.y}
        selected={selected}
        hoverValue={hoverValue}
      />
    </svg>
  );
}

export function Chart() {
  const [selected, setSelected] = useState("A");
  const data = rawData[selected];

  ... // chart things here

  return (
    <div>
      <Select selected={selected} setSelected={setSelected} />
      <ChartSVG
        data={data}
        selected={selected}
      >
        <Axes data={data} />
        <Line data={data} />
        {children}
      </ChartSVG>
    </div>
  );
}

0100200300400500600700050100150200250300350400A: 284

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 difference. And finally, always measure before moving code in the name of performance!