React: Lazy useRef instance variables

Created on 24 Dec 2018  Â·  14Comments  Â·  Source: facebook/react

I want to save a class instance, not just plain object, with useRef hook. And, I dont want to construct it each time hook function is invoked.

Is it possible to do that?

My thoughts were:

const instanceRef = useRef(() => new Instance())

But useRef does not accept the _initial value_ as a function (useState and useReducer could do that).
Will that change in _alpha 3_?

Most helpful comment

@Diokuz Just check its existence

const instance = React.useRef(null)
if (instance.current == null) {
  instance.current = {
    // whatever you need
  }
}

All 14 comments

@Diokuz useRef does take in an initial state.

https://reactjs.org/docs/hooks-reference.html#useref

In the above link, they just pass null as the initial value.

@nikilok yes, but the question was about how to create lazy initial value (create it only once).

My current solution is:

const [refInitialValue] = useState(() => new Instance)
const instanceRef = useRef(refInitialValue)

but that is a bit overhead.

@Diokuz Just check its existence

const instance = React.useRef(null)
if (instance.current == null) {
  instance.current = {
    // whatever you need
  }
}

I created this custom hook to help with this limitation, someone might find it useful:

import { useRef } from 'react';

const SENTINEL = {};

export function useRefFn<T>(init: () => T) {
    const ref = useRef<T | typeof SENTINEL>(SENTINEL);
    if (ref.current === SENTINEL) {
        ref.current = init();
    }
    return ref as React.MutableRefObject<T>;
}

It happens to work in this case because your function is generic and so T is "like {}", but you need to use a Symbol for sentinels to create a unique symbol type. Using an empty object's type as an input makes it nearly the same as any but without null | undefined.

Sorry, I'm not following. It would work for null, undefined, or any value as far as I can see.

I use an empty object as the sentinel as it makes it unique and distinct for the === check which sets the ref to the real initial value on the first invocation. It'd work it the sentinel were a symbol too, but I don't think it makes a difference one way or the other?

Late realisation: doesn't useMemo (with an empty array as the second parameter) do exactly this?

useMemo with [] is not recommended for this use case. In the future we will likely have use cases where we drop useMemo values — to free memory or to reduce how much we retain with e.g. virtual scrolling for hidden items. You shouldn't rely on useMemo retaining a value.

We talked more about this and settled on this pattern as the recommendation for expensive objects:

function Foo() {
  const instanceRef = useRef(null)

  function getInstance() {
    let instance = instanceRef.current;
    if (instance !== null) {
      return instance;
    }
    // Lazy init
    let newInstance = new Instance()
    instanceRef.current = newInstance;
    return newInstance;
  }

  // Whenever you need it...
  const instance = getInstance();
  // ...
}

Note how you can declare getInstance() as : Instance — it's easy to guarantee it's never null.

So you both get type safety and delay creation until the object is actually necessary (which may happen conditionally during rendering, or during an effect, etc).

Hope this helps!

https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily

How about:

const [myRef] = useState(() => ({ current: calculateInitialValue() }))

Is there any reason not to "abuse" useState like that?

// @flow

import {useRef} from "react";

type HookRef<Value> = {|
  current: Value
|};

const noValue = Symbol("lazyRef.noValue");

const useLazyRef = <Value>(getInitialValue: () => Value): HookRef<Value> => {
  const lazyRef = useRef(noValue);

  if (lazyRef.current === noValue) {
    lazyRef.current = getInitialValue();
  }

  return lazyRef;
};

export default useLazyRef;

I second @ghengeveld's question above about "abusing" setState for this purpose especially given Dan's comment here saying that useRef is basically useState anyway. The official recommendation seems much less concise. @gaearon, could you clarify?

The benefit of Dan's approach is that the instance is created once, at the point where it's going to be used (if at all), while my solution just instantiates/creates it immediately (also only once). I think my pattern is fine if you know for certain you are going to need the value. Unless there is a reason to avoid this trick, like there is for useMemo?

You can also just pass a function that mutates a ref to useMemo without being at risk of changing semantics WRT retained values:

const initializeRef = (ref: React.MutableRefObject<any>) => {
     if (!ref.current) {
         ref.current = init();
     }
}

// in your component
const ref = useRef(null)
useMemo(() => {
    initializeRef(ref)
}, [])

The handy thing about this is that you can also re-run initialisation of the ref if needed by supplying deps.

Was this page helpful?
0 / 5 - 0 ratings