Skip to content

TkDodo's blog

useState for one-time initializations

β€” React, useState pitfalls, useMemo β€” 2 min read

use state pitfalls

When we talk about memoization and keeping references stable, useMemo is usually the first thing that comes to mind. I'm not in the mood for writing much text today,so I'm just gonna lead with a (real-world) example that happened to me this week:

The Example

Suppose you have a resource that you only want to initialize once per life-time of your app. The recommended pattern is usually to create the instance outside of the component:

static-instance
1// βœ… static instance is only created once
2const resource = new Resource()
3
4const Component = () => (
5 <ResourceProvider resource={resource}>
6 <App />
7 </ResourceProvider>
8)

The const resource is created once when the js bundle is evaluated, and then made available to our app via the ResourceProvider. So far, so good. This usually works well for resources that you need once per App, like redux stores.

In our case however, we were mounting the Component (a micro-frontend) multiple times, and each one needs their own resource. All hell breaks loose if two of those share the same resource. So we needed to move it into the Component:

create-in-component
1const Component = () => {
2 // 🚨 be aware: new instance is created every render
3 const resource = new Resource()
4 return (
5 <ResourceProvider resource={new Resource()}>
6 <App />
7 </ResourceProvider>
8 )
9}

I think it is rather obvious that this is not a good idea. The render function now creates a new Resource on every render! This will coincidentally work if we only render our Component once, but this is nothing you should ever rely on. Re-renders can (and likely will) happen, so be prepared!

The first solution that came to our mind was to useMemo. After all, useMemo is for only re-computing values if dependencies change, and we don't have a dependency here, so this looked wonderful:

use-memo
1const Component = () => {
2 // 🚨 still not truly stable
3 const resource = React.useMemo(() => new Resource(), [])
4 return (
5 <ResourceProvider resource={resource}>
6 <App />
7 </ResourceProvider>
8 )
9}

Again, this might coincidentally work for some time, but let's have a look at what the react docs have to say about useMemo:

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to β€œforget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo β€” and then add it to optimize performance.

Wait, what? If we should write our code in a way that it still works without useMemo, we are basically not making our code any better by adding it. We are not really concerned about performance here, we want true referential stability please. What's the best way to achieve this?

state to the rescue

Turns out, it's state. State is guaranteed to only update if you call the setter. So all we need to do is not call the setter, and since it's the second part of the returned tuple, we can just not destruct it. We can even combine this very well with the lazy initializer to make sure the resource constructor is only invoked once:

use-state
1const Component = () => {
2 // βœ… truly stable
3 const [resource] = React.useState(() => new Resource())
4 return (
5 <ResourceProvider resource={resource}>
6 <App />
7 </ResourceProvider>
8 )
9}

With this trick, we will make sure that our resource is truly only created once per component lifecycle πŸš€.

what about refs?

I think you can achieve the same with useRef, and according to the rules of react, this wouldn't even break purity of the render function:

use-ref
1const Component = () => {
2 // βœ… also works, but meh
3 const resource = React.useRef(null)
4 if (!resource.current) {
5 resource.current = new Resource()
6 }
7 return (
8 <ResourceProvider resource={resource.current}>
9 <App />
10 </ResourceProvider>
11 )
12}

Honestly, I don't know why you should do it this way - I think this looks rather convoluted, and TypeScript will also not like it, because resource.current can technically be null. I prefer to just useState for these cases.


Leave a comment below ⬇️ or reach out to me on twitter if you have any questions

Β© 2021 by TkDodo's blog. All rights reserved.
Theme by LekoArts