Reputation: 118761
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);
}, []);
...
}
Upvotes: 13
Views: 4513
Reputation: 33439
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();
}
....
Upvotes: 10
Reputation: 203476
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.
Upvotes: 0
Reputation: 169388
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.
Upvotes: 2