Search code examples
reactjsreact-hooksreact-strictmode

Is it safe to change a ref's value during render instead of in useEffect?


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);
  }, []);

  ...
}

Solution

  • 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();
      }
      ....