Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] useRef to reduce dependencies in useEffect/useCallback #16091

Closed
dangcuuson opened this issue Jul 9, 2019 · 3 comments

Comments

@dangcuuson
Copy link

commented Jul 9, 2019

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?

@dangcuuson dangcuuson changed the title [Question] useRef to reduce dependecies in useEffect/useCallback [Question] useRef to reduce dependencies in useEffect/useCallback Jul 10, 2019

@butchler

This comment has been minimized.

Copy link

commented Jul 18, 2019

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: #15278 (comment)

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;
}
@jquense

This comment has been minimized.

Copy link
Collaborator

commented Jul 18, 2019

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

@dangcuuson

This comment has been minimized.

Copy link
Author

commented Jul 18, 2019

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.