9

I'm using useRef to hold the latest value of a prop so that I can access it later in an asynchronously-invoked callback (such as an onClick handler). I'm using a ref instead of putting value in the useCallback dependencies list because I expect the value will change frequently (when this component is re-rendered with a new value), but the onClick handler will be called rarely, so it's not worth assigning a new event listener to an element each time the value changes.

function MyComponent({ value }) {
  const valueRef = useRef(value);
  valueRef.current = value;  // is this ok?

  const onClick = useCallback(() => {
    console.log("the latest value is", valueRef.current);
  }, []);

  ...
}

The documentation for React Strict Mode leads me to believe that performing side effects in render() is generally unsafe.

Because the above methods [including class component render() and function component bodies] might be called more than once, it’s important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems, including memory leaks and invalid application state.

And in fact I have run into problems in Strict Mode when I was using a ref to access an older value.

My question is: Is there any concern with the "side effect" of assigning valueRef.current = value from the render function? For example, is there any situation where the callback would receive a stale value (or a value from a "future" render that hasn't been committed)?

One alternative I can think of would be a useEffect to ensure the ref is updated after the component renders, but on the surface this looks unnecessary.

function MyComponent({ value }) {
  const valueRef = useRef(value);
  useEffect(() => {
    valueRef.current = value;  // is this any safer/different?
  }, [value]);

  const onClick = useCallback(() => {
    console.log("the latest value is", valueRef.current);
  }, []);

  ...
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
jtbandes
  • 115,675
  • 35
  • 233
  • 266

3 Answers3

6

For example, is there any situation where the callback would receive a stale value (or a value from a "future" render that hasn't been committed)?

The parenthetical is the primary concern.

There's currently a one-to-one correspondence between render (and functional component) calls and actual DOM updates. (i.e. committing)

But for a long time the React team has been talking about a "Concurrent Mode" where an update might start (render gets called), but then get interrupted by a higher priority update.

In this sort of situation it's possible for the ref to end up out of sync with the actual state of the rendered component, if it's updated in a render that gets cancelled.

This has been hypothetical for a long time, but it's just been announced that some of the Concurrent Mode changes will land in React 18, in an opt-in sort of way, with the startTransition API. (And maybe some others)


Realistically, how much this is a practical concern? It's hard to say. startTransition is opt-in so if you don't use it you're probably safe. And many ref updates are going to be fairly 'safe' anyway.

But it may be best to err on the side of caution, if you can.


UPDATE: Now, the react.dev docs also say you should not do it:

Do not write or read ref.current during rendering, except for initialization. This makes your component’s behavior unpredictable.

By initialization above they mean such pattern:

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  ....
mbelsky
  • 6,093
  • 2
  • 26
  • 34
Retsam
  • 30,909
  • 11
  • 68
  • 90
  • Thanks. This concurrent mode issue is one of the things I was wondering about. Is it possible to construct an example today that would trigger an issue with this? – jtbandes Jun 17 '21 at 20:18
  • Thanks, with this new information I am comfortable accepting this answer. @GiorgiMoniava do you know why they say you also should not _read_ the ref during render? – jtbandes Dec 19 '22 at 18:21
  • @jtbandes Sadly I don't but I think it is related to purity – Giorgi Moniava Dec 19 '22 at 18:55
0

To the best of my knowledge, it is safe, but you just need to be aware that changes to the ref-boxed value may occur when React "feels like" rendering your component and not necessarily deterministically.

This looks a lot like react-use's useLatest hook (docs), reproduced here since it's trivial:

import { useRef } from 'react';

const useLatest = <T>(value: T): { readonly current: T } => {
  const ref = useRef(value);
  ref.current = value;
  return ref;
};

export default useLatest;

If it works for react-use, I think it's fine for you too.

AKX
  • 152,115
  • 15
  • 115
  • 172
  • Can you expand on _"may occur when React "feels like" rendering your component and not necessarily deterministically"_? How might I (perhaps unintentionally) observe this nondeterminism? – jtbandes Jun 17 '21 at 20:00
  • I _think_ there could be a time where the ref'd value has changed but changes haven't been flushed to the actual browser DOM, but considering event handlers are fired in that same work loop, I don't _think_ there's much of a chance (if any at all) for a sequence where an interaction event occurs, React renders the component & changes the ref, and only then is your handler fired with the "future" ref value... – AKX Jun 17 '21 at 20:04
  • That's my feeling too... with this question I was hoping for some kind of proof (documentation? or just convincing argument?) or a counterexample :) – jtbandes Jun 17 '21 at 20:57
0
function MyComponent({ value }) {
  const valueRef = useRef(value);
  valueRef.current = value;  // is this ok?

  const onClick = useCallback(() => {
    console.log("the latest value is", valueRef.current);
  }, []);

  ...
}

I don't really see an issue here as valueRef.current = value will occur every render cycle. It's not expensive, but it will happen every render cycle.

If you use an useEffect hook then you at least minify the number of times you set the ref value to only when the prop actually changes.

function MyComponent({ value }) {
  const valueRef = useRef(value);
  useEffect(() => {
    valueRef.current = value;
  }, [value]);

  const onClick = useCallback(() => {
    console.log("the latest value is", valueRef.current);
  }, []);

  ...
}

Because of the way useEffect works with the component lifecycle I'd recommend sticking to using the useEffect hook and keeping normal React patterns. Using the useEffect hook also provides a more deterministic value per real render cycle, i.e. the "commit phase" versus the "render phase" that can be cancelled, aborted, recalled, etc...

Curious though, if you just want the latest value prop value, just reference the value prop directly, it will always be the current latest value. Add it to the useCallback hook's dependency. This is essentially what you are accomplishing with the useEffect to update the ref, but in a clearer manner.

function MyComponent({ value }) {
  ...

  const onClick = useCallback(() => {
    console.log("the latest value is", value);
  }, [value]);

  ...
}

If you really just always want the latest mutated value then yeah, skip the useCallback dependencies, and skip the useEffect, and mutate the ref all you want/need and just reference whatever the current ref value is at the time the callback is invoked.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • "Add it to the useCallback hook's dependency" — yep, I would often do this, but I did include a sentence about this in my post: "I expect the value will change frequently, but the onClick handler will be called rarely, so it's not worth assigning a new event listener to an element each time the value changes". – jtbandes Jun 17 '21 at 20:17
  • @jtbandes So you've *other* code that is *possibly* mutating the ref value *during* a render cycle? (*Oof*) Yeah, skip the dependencies, and skip the `useEffect` and mutate the ref all you want and just reference whatever the current ref value is at the time the callback is invoked. – Drew Reese Jun 17 '21 at 20:20
  • Sorry, I didn't mean that the ref is being mutated — I just meant this component is being re-rendered frequently with a new `value` prop. In more complex examples, there can be a large subtree that I want to avoid re-rendering each time, and that's why I wanted to avoid changing the callback identity. – jtbandes Jun 17 '21 at 20:25
  • @jtbandes If a parent component is rerendered, its subtree is also rerendered in order to compute a diff for reconciliation. React is optimized for performance pretty well out-of-the-box. Do you have an actual performance issue that you are solving for? Using the hook dependencies helps. There is also the `memo` HOC can help children components possibly rerender less often. – Drew Reese Jun 17 '21 at 20:36
  • On another note – "you at least minify the number of times you set the ref value to only when the prop actually changes" – this seems misleading to me as it's almost certainly more expensive (although still fairly cheap) to useEffect which has to bind a new closure, interact with Hooks internals, and compare deps, rather than a single object property assignment. – jtbandes Jun 17 '21 at 21:06