Skip to content
Advertisement

React’s useRef hook doesn’t take a function?

I’m used to passing functions to useState, so that i don’t create unnecessary objects:

useState(() => /* create complex obj */)

I expected that useRef would work the same way, but the below returns a function instead of calling it once to intialize, then returning the prev created object after that.

useRef(() => /* create complex obj */).current

I suppose one could do something like this, but seems a lot less clean.

const myRef = useRef();
useEffect(() => {
    myRef.current = /* create complex obj */;
}, []);

Am I missing something or is it really a restriction of useRef?

Update

To clarify, this is the usual way to use useState and useRef:

useState(createSimpleInitialValue());
useRef(createSimpleInitialValue());

For each render, you’re spending time creating an initial value that will just be discarded after the first pass. This doesn’t matter for simple objects, but in the case complex objects it can sometimes be a problem. useState has a workaround:

useState(() => createComplexObj());

Instead of an object, we pass a function. React will invoke the function on the first render, but will not on subsequent passes, so you only have to build the object once. I hoped useRef would have such a feature, but when you pass a function it just stores the function. The docs don’t mention that useRef can take a function, but I was hoping there was still some built in way to do it.

Advertisement

Answer

As I pointed out in the comments, there is already quite a long discussion of this on the React Github repository. Several workaround are suggested, including ones using useMemo with an empty dependency array – but this is specifically not recommended by Dan Abramov (one of the core React developers) in this comment, because

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.

But he goes on to offer his own recommended workaround:

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

with a code snippet which I’ve represented the essentials of below while removing details (which can still be seen at the Github link above if interested) that are specific to the use-case of the raiser of the issue and matching the brief code samples in your question better:

function MyComponent() {
  const myRef = useRef(null)

  function getComplexObject() {
    let complexObject = myRef.current;
    if (complexObject !== null) {
      return complexObject;
    }
    // Lazy init
    let newObject = /* create complex obj */;
    myRef.current = newObject;
    return newObject;
  }

  // Whenever you need it...
  const complexObject = getComplexObject();
  // ...
}

As you can hopefully see, the idea here is simple even though the code is a little verbose: we initialise the ref value to null and then, whenever it is needed, calculate it if the ref holds null and store it in the ref, otherwise use the value stored in the ref. It’s just a very basic memoisation but, unlike React’s useMemo, is completely guaranteed to not recalculate the value after the first render.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement