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 is only created once
const resource = new Resource()
const Component = () => (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
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:
const Component = () => {
// 🚨 be aware: new instance is created every render
const resource = new Resource()
return (
<ResourceProvider resource={new Resource()}>
<App />
</ResourceProvider>
)
}
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:
const Component = () => {
// 🚨 still not truly stable
const resource = React.useMemo(() => new Resource(), [])
return (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
}
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:
const Component = () => {
// ✅ truly stable
const [resource] = React.useState(() => new Resource())
return (
<ResourceProvider resource={resource}>
<App />
</ResourceProvider>
)
}
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:
const Component = () => {
// ✅ also works, but meh
const resource = React.useRef(null)
if (!resource.current) {
resource.current = new Resource()
}
return (
<ResourceProvider resource={resource.current}>
<App />
</ResourceProvider>
)
}
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
Top comments (4)
FYI
While possible, there is no lazy initializer for
useRef
. So in this case, the resource constructor will be called on every render and the result will be discarded. This is certainly possible for some cases, but it’s not the same :)I haven't tested it myself, but I have my doubts. From sources:
Tested, you are right. It looks like they talk about the object itself, but do not guarantee that it will be persisted in
.current
. Well, good to know.