Join Kent for a live workshop in New York, USA 🗽

React Server Components and Actions
Time's up. The sale is over

State Colocation will make your React app faster

September 23rd, 2019 — 11 min read

by Samuel Zeller
by Samuel Zeller

Watch "Lifting and colocating React State" on egghead.io (part of The Beginner's Guide to ReactJS).

One of the leading causes to slow React applications is global state, especially the rapidly changing variety. Allow me to illustrate my point with a super contrived example, then I'll give you a slightly more realistic example so you can determine how it can be more practically applicable in your own app.


enter a dog name

Wow, that was ms slow

Here's the code for that

function sleep(time) {
	const done = Date.now() + time
	while (done > Date.now()) {
		// sleep...
	}
}

// imagine that this slow component is actually slow because it's rendering a
// lot of data (for example).
function SlowComponent({ time, onChange }) {
	sleep(time)
	return (
		<div>
			Wow, that was{' '}
			<input
				value={time}
				type="number"
				onChange={(e) => onChange(Number(e.target.value))}
			/>
			ms slow
		</div>
	)
}

function DogName({ time, dog, onChange }) {
	return (
		<div>
			<label htmlFor="dog">Dog Name</label>
			<br />
			<input id="dog" value={dog} onChange={(e) => onChange(e.target.value)} />
			<p>{dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}</p>
		</div>
	)
}

function App() {
	// this is "global state"
	const [dog, setDog] = React.useState('')
	const [time, setTime] = React.useState(200)
	return (
		<div>
			<DogName time={time} dog={dog} onChange={setDog} />
			<SlowComponent time={time} onChange={setTime} />
		</div>
	)
}

Play around that for a second and you'll notice a significant performance problem when you interact with either field. There are various things that we can do to improve the performance of both the DogName and SlowComponent components on their own. We could pull out the rendering bailout escape hatches like React.memo and apply that all over our codebase where we have slow renders. But I'd like to propose an alternative solution.

If you haven't already read Colocation, then I suggest you give that a read. Knowing that colocation can improve the maintenance of our application, let's try colocating some state. Observe that the time state is used by every component in the App, which is why it was lifted to the App. However the dog state is only used by one component, so let's move that state to be colocated (updated lines are highlighted):

function DogName({ time }) {
	const [dog, setDog] = React.useState('')
	return (
		<div>
			<label htmlFor="dog">Dog Name</label>
			<br />
			<input id="dog" value={dog} onChange={(e) => setDog(e.target.value)} />
			<p>{dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}</p>
		</div>
	)
}

function App() {
	// this is "global state"
	const [time, setTime] = React.useState(200)
	return (
		<div>
			<DogName time={time} />
			<SlowComponent time={time} onChange={setTime} />
		</div>
	)
}

And here's the result:


enter a dog name

Wow, that was ms slow

Wow! Typing in the dog name input is WAY better now. And what's more, the component's easier to maintain thanks to colocation. But how did it get faster?

I've heard it said that the best way to make something fast is to do less stuff. That's exactly what's going on here. When we manage the state higher up in the React component tree, every update to that state results in an invalidation of the entire React tree. React doesn't know what's changed, so it has to go and check all the components to determine whether they need DOM updates. That process is not free (especially when you have arbitrarily slow components). But if you move your state further down the React tree as we did with the dog state and the DogName component, then React has less to check. It doesn't even bother calling our SlowComponent because it knows that there's no way that could have changed output because it can't reference the changed state anyway.

In short, before, when we changed the dog name, every component had to be checked for changes (re-rendered). After, only the DogName component needed to be checked. This resulted in a big performance win! Sweet!

Real World

Where I see this principle apply in real-world applications is when people put things into a global Redux store or in a global context that don't really need to be global. Inputs like the DogName in the example above are often the places where this perf issue manifests itself, but I've also seen it happen plenty on mouse interactions as well (like showing a tooltip over a graph or table of data).

Often the solution that people try for this kind of problem is to "debounce" the user interaction (ie wait for the user to stop typing before applying the state update). This is sometimes the best we can do, but it definitely leads to a sub-optimal user experience (React's upcoming concurrent mode should make this less necessary in the future. Watch this demo from Dan about it).

Another solution people try is to apply one of React's rendering bailout escape hatches like React.memo. This works pretty well in our contrived example because it allows React to skip re-rendering our SlowComponent, but in a more practical scenario, you often suffer from "death by a thousand cuts" which means that there's not really a single place that's slow, so you wind up applying React.memo everywhere. And when you do that, you have to start using useMemo and useCallback everywhere as well (otherwise you undo all the work you put into React.memo). Each of these optimizations together may solve the problem, but it drastically increases the complexity of your application's code and it actually is less effective at solving the problem than colocating state because React does still need to run through every component from the top to determine whether it should re-render. You'll definitely be running more code with this approach, there's no way around that.

If you'd like to play around with a slightly less contrived example, give this codesandbox a look.

What is colocated state?

The principle of colocation is:

Place code as close to where it's relevant as possible

So, to accomplish this, we had our dog state inside the DogName component:

function DogName({ time }) {
	const [dog, setDog] = React.useState('')
	return (
		<div>
			<label htmlFor="dog">Dog Name</label>
			<br />
			<input id="dog" value={dog} onChange={(e) => setDog(e.target.value)} />
			<p>{dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}</p>
		</div>
	)
}

But what happens when we break that up? Where does that state go? The answer is the same: "as close to where it's relevant as possible." That would be the closest common parent. As an example, let's break the DogName component up so the input and the p show up in different components:

function DogName({ time }) {
	const [dog, setDog] = React.useState('')
	return (
		<div>
			<DogInput dog={dog} onChange={setDog} />
			<DogFavoriteNumberDisplay time={time} dog={dog} />
		</div>
	)
}

function DogInput({ dog, onChange }) {
	return (
		<>
			<label htmlFor="dog">Dog Name</label>
			<br />
			<input id="dog" value={dog} onChange={(e) => onChange(e.target.value)} />
		</>
	)
}

function DogFavoriteNumberDisplay({ time, dog }) {
	return (
		<p>{dog ? `${dog}'s favorite number is ${time}.` : 'enter a dog name'}</p>
	)
}

In this case we can't move the state to the DogInput component, because the DogFavoriteNumberDisplay needs access to that state, so we navigate up the tree until we find the least common parent of these two components and that's where the state is managed.

And this applies just as well as if your state needs to be accessed in dozens of components on a specific screen of your application. You can even put it into context to avoid prop drilling if you want. But keep that context value provider as close to where it's relevant as possible and you'll still benefit from the performance (and maintenance) characteristics of colocation. By this I mean that while some of your context providers could be rendered at the top of your application's React tree, they don't all have to be there. You can put them wherever they make the most sense.

This is the essence of what my Application State Management with React blog post is all about. Keep your state as close to where it's used as possible, and you'll benefit from a maintenance perspective and a performance perspective. From there, the only performance concerns you should have is the occasional especially complex UI interaction.

What about context or Redux?

If you read "One simple trick to optimize React re-renders," then you know that you can make it so only components that actually use the changing state will be updated. So that can side step this issue. While this is true, people do still have performance problems with Redux. If it's not React itself, what is it? The problem is that React-Redux expects you to follow guidelines to avoid unnecessary renders of connected components, and it can be easy to accidentally set up components that render too often when other global state changes. The impact of that becomes worse and worse as your app grows larger, especially if you're putting too much state into Redux.

Fortunately, there are things you can do to help reduce the impact of these performance issues, like using memoized Reselect selectors to optimize mapState functions, and the Redux docs have additional info on improving performance of Redux apps.

I also want to note that you can definitely apply colocation with Redux to get these benefits as well. Just limit what you store in Redux to be actual global state and colocate everything else and you're golden. The Redux FAQ has some rules of thumb to help decide whether state should go in Redux, or stay in a component.

In addition, if you separate your state by domain (by having multiple domain-specific contexts), then the problem is less pronounced as well.

But the fact remains that if you colocate your state, you don't have these problems and maintenance is improved.

So how do you decide where to put state?

I made this decision tree chart to help:

where to put react state

Chart perfected by

Stephan Meijer


Here's that written out (for screen readers and friends):

  • 1 Start building an app. Go to 2
  • 2 useState. Go to 3
  • 3 used by only this component?
    • Yes: Go to 4
    • No: used by only one child?
      • Yes: Colocate state. Go to 3
      • No: used by a sibling/parent?
        • Yes: Lift state. Go to 3
        • No: Go to 4
  • 4 Leave it. Go to 5
  • 5 having a "prop drilling" problem?
    • Yes: Can child function outside of parent?
      • Yes: Move State to Context Provider. Go to 6
      • No: Use Component Composition. Go to 6
    • No: Go to 6
  • 6 Ship the app. As requirements change, Go to 1

It's important that this is something you do as part of your regular refactoring/app maintenance process. This is because lifting state up is a requirement of getting this working so it happens naturally, but your app will "work" whether you colocate your state or not, so being intentional about thinking through this is important to keep your app manageable and fast.

If you want to learn a bit more about that component composition step, read about that in One React mistake that's slowing you down.

Conclusion

In general, I think people are pretty good at "lifting state" as things change, but we don't often think to "colocate" state as things change in our codebase. So my challenge to you is to look through your codebase and look for opportunities to colocate state. Ask yourself "do I really need the modal's status (open/closed) state to be in Redux?" (the answer is probably "no"). Colocate your state and you'll find yourself with a faster, simpler codebase. Good luck!

P.S. I wrote another article that covers this topic from the perspective of forms and "perf death by a thousand cuts". Head on over to EpicReact.dev to read: Improve the Performance of your React Forms

Epic React

Get Really Good at React

Illustration of a Rocket
Kent C. Dodds
Written by Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. Kent's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Learn more about Kent

Want to learn more?

Join Kent in a live workshop

If you found this article helpful.

You will love these ones as well.