DEV Community

loading...
Cover image for useState for one-time initializations

useState for one-time initializations

tkdodo profile image Dominik D Originally published at tkdodo.eu ・3 min read

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>
)
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (4)

pic
Editor guide
Collapse
xr0master profile image
Sergey Khomushin • Edited
const resource = React.useRef<Resource>(new Resource());
Enter fullscreen mode Exit fullscreen mode

FYI

Collapse
tkdodo profile image
Dominik D Author

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 :)

Collapse
xr0master profile image
Sergey Khomushin • Edited

I haven't tested it myself, but I have my doubts. From sources:

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

Thread Thread
xr0master profile image
Sergey Khomushin

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.