useState for one-time initializations
— React, useState pitfalls, useMemo — 2 min read
- #1: Don't over useState
- #2: Putting props to useState
- #3: Things to know about useState
- #4: useState for one-time initializations
- #5: useState vs. useReducer
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:
1// ✅ static instance is only created once2const 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:
1const Component = () => {2 // 🚨 be aware: new instance is created every render3 const resource = new Resource()4 return (5 <ResourceProvider resource={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:
1const Component = () => {2 // 🚨 still not truly stable3 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:
1const Component = () => {2 // ✅ truly stable3 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:
1const Component = () => {2 // ✅ also works, but meh3 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 bluesky if you have any questions.