React: [Question] useRef to reduce dependencies in useEffect/useCallback

Created on 9 Jul 2019  路  3Comments  路  Source: facebook/react

Firstly, sorry for putting a question in the issues tracker, since I'm not sure where to put this.

I really love the concept of hooks & have been converting many class components to hooks. One of the common problems I usually get with hooks, is to access the latest state/props in useEffect and useCallback, while avoiding specify too many dependencies to these hooks.

(I'm aware of exhausive deps, but for optimization, I don't want useEffect to be called too many times, or useCallback to return a different function every times)

For example, I want to maintain the identity of function returned from useCallback, so I'd need to put [] as 2nd argument (because I'm gonna pass it to a component inside React.memo and don't want to write custom props comparator). So it'd be like this:

useCallback(() => doStuff(value1, value2), [])

Of course it won't work because doStuff will always receive initial value of value1 and value2. That's why I'd need to use useRef:

const ref = useRef({ value1, value2 });
ref.current = { value1, value2 };
useCallback(() => doStuff(ref.current.value1, ref.current.value2), [])

And could say that this happens so many time that I decided to write a custom hook for it :)

export function useCallbackWithRef<TRef, TCb extends (...args: any[]) => any>(
  refData: TRef,
  callback: (refData: TRef) => TCb
): TCb {
  const ref = React.useRef(refData);
  ref.current = refData;
  return React.useCallback((...args: any[]) => {
    return callback(ref.current)(...args);
  }, []) as TCb;
}

//usage
useCallbackWithRef({ value1, value2 }, ref => () => doStuff(ref.value1, ref.value2))

And it seems to work quite nice: Codesandbox

I'm going to use this across many places in my project, but I don't want to have many regrets later on so I just want to ask a few things:

  1. Is there any performance issue with excessive use of useRef? Since they're just pointer to an already exist object, I guess it's not going to have any memory impact?
  2. I read somewhere that React may decide to re-compute value in useMemo if needed, even if I specify [] as dependencies. Is it better if I change the implementation to useState with lazy init, instead of useCallback?
  3. Or is there a much more simpler, a true React way to achieve what I want but I have overlooked?

Most helpful comment

I think mutating the ref value as a side effect of render like this:

const ref = useRef({ value1, value2 });
ref.current = { value1, value2 };

could cause issues in concurrent mode, since the render function could be called multiple times with different, intermediate values before the virtual DOM is committed to the DOM, which could potentially lead to confusing bugs. I don't fully understand it myself, but see this discussion for a little more context: https://github.com/facebook/react/issues/15278#issuecomment-478427588

Just a heads up that this is likely to be problematic in concurrent mode, since the function might be called many times with different props.

However, just using useEffect might be enough to make it safe for concurrent mode:

const ref = useRef({ value1, value 2});
useEffect(() => {
  ref.current = { value1, value2 };
});

This could also be packaged as a custom hook if it's useful (I have run into this kind of use case myself a few times as well):

import { useRef, useEffect } from 'react';

export default function useLatest(value) {
  const valueRef = useRef(value);
  useEffect(() => {
    valueRef.current = value;
  }, [value]);
  return valueRef;
}

All 3 comments

I think mutating the ref value as a side effect of render like this:

const ref = useRef({ value1, value2 });
ref.current = { value1, value2 };

could cause issues in concurrent mode, since the render function could be called multiple times with different, intermediate values before the virtual DOM is committed to the DOM, which could potentially lead to confusing bugs. I don't fully understand it myself, but see this discussion for a little more context: https://github.com/facebook/react/issues/15278#issuecomment-478427588

Just a heads up that this is likely to be problematic in concurrent mode, since the function might be called many times with different props.

However, just using useEffect might be enough to make it safe for concurrent mode:

const ref = useRef({ value1, value 2});
useEffect(() => {
  ref.current = { value1, value2 };
});

This could also be packaged as a custom hook if it's useful (I have run into this kind of use case myself a few times as well):

import { useRef, useEffect } from 'react';

export default function useLatest(value) {
  const valueRef = useRef(value);
  useEffect(() => {
    valueRef.current = value;
  }, [value]);
  return valueRef;
}

if anyone is interested in an already packaged version: https://github.com/react-restart/hooks/blob/master/src/useCommittedRef.ts

Thank you for the answer. I like the useLatest/useCommitedRef solution more than my useCallbackWithRef since they're more generic and ... atomic? (I hope you know what I mean)

Was this page helpful?
0 / 5 - 0 ratings