React's useRef hook doesn't take a function?
Asked Answered
O

4

6

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.

Occasionalism answered 11/10, 2022 at 20:36 Comment(3)
Not really clear on what you're asking, but you should probably read the documentation for a misunderstanding like this: reactjs.org/docs/hooks-reference.html#userefCheekbone
this is much discussed, with comments/suggestions from the React team (notably gaeron) hereCorduroy
@C.Helling I updated the question to clarify.Occasionalism
C
8

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.

Corduroy answered 11/10, 2022 at 21:46 Comment(0)
T
3

Explicit quote from React 18 docs

https://18.react.dev/reference/react/useRef#avoiding-recreating-the-ref-content now confirms what was previously mentioned at: https://mcmap.net/q/1662698/-react-39-s-useref-hook-doesn-39-t-take-a-function

Avoiding recreating the ref contents

React saves the initial ref value once and ignores it on the next renders.

function Video() {
  const playerRef = useRef(new VideoPlayer());
  // ...

Although the result of new VideoPlayer() is only used for the initial render, you’re still calling this function on every render. This can be wasteful if it’s creating expensive objects.

To solve it, you may initialize the ref like this instead:

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  // ...

Normally, writing or reading ref.current during render is not allowed. However, it’s fine in this case because the result is always the same, and the condition only executes during initialization so it’s fully predictable.

Its because of stuff like this that I'll never fully understand React, it just requires 2x my brain.

Toon answered 16/5 at 15:22 Comment(0)
P
1

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

Yes, the useState hook takes a value or if passed a function it is assumed to be an initialization function that returns the initial state value. This is not the case with the useRef hook. It's probably easiest to think of a React ref as a mutable bucket that can hold anything and be updated at any time. In this case if you pass a function to the useRef hook it will happily store the function as the current value and you can recall the value later.

In other words if you want to use a function to create and return a value to store in the bucket it needs to be invoked first so the returned value is what is stored instead of the function.


Demo Purposes Only - Don't Actually Do This

Immediately Invoked Function Expression (IIFE) example:

const myRef = useRef((() => {
  /* create & return complex obj */
})());

External initializer function example:

const createComplexObject = () => {
  /* create & return complex obj */
};

...

const myRef = useRef(createComplexObject());

This will certainly work as you are expecting it to, the ref value will initially be that of the complex object, but a likely unexpected effect will be that the functions used to do this will be called on each and every render cycle.

This is only to showcase that it is technically possible, but because of the side-effect it's not the ideal way to initialize a useRef hook value.

Demo Purposes Only - Don't Actually Do This

const createComplexObject = () => {
  console.log("useRef createComplexObject function");
  return 42;
};

function App() {
  const ref = React.useRef(
    (() => {
      console.log("useRef IIFE function");
      return 42;
    })()
  );

  const ref2 = React.useRef(createComplexObject());

  const [count, setCount] = React.useState(0);
  const increment = () => setCount((c) => c + 1);

  return (
    <div className="App">
      <div>Count: {count}</div>
      <div>Ref: {ref.current}</div>
      <button type="button" onClick={increment}>+</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root" />

OK to do this

The useEffect method with proper dependency is an acceptable method of instantiating the ref.

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

useMemo is a close next candidate but because of the points in Robin's comments and answer is also not recommended. Robin's answer does a great job at providing an alternative solution to the useEffect solution.

Pacifism answered 12/10, 2022 at 4:41 Comment(0)
C
0

If you don't need to modify the object after initialization, how about using useState hook ignoring the setter function instead of useRef?

Ex: const [myObj] = useState(initFunction) //intFunction is the functional reference

Ceyx answered 14/8 at 0:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.