7

I'm reading about hooks in React, and I have some trouble understanding the difference between useRef and useCallback hooks.

Specifically, I'm looking to understand how these two can be used to avoid unnecessary re-renders of child components.

Based on my understanding of this answer on stack overflow, the following function can be written as:

  const Avatar = function({ history, url, fullName }) {
  const onMenuItemClick = urlToNavigate => history.push(urlToNavigate),
    onMenuItemClickRef = useRef(onMenuItemClick);

  return (
    <Menu
      label={<RoundedImage src={url} alt={fullName} />}
      items={[
        { label: `Logged in as: ${fullName}` },
        {
          label: "Liked articles",
          onClick: () => onMenuItemClickRef.current("/liked-articles")
        },
        {
          label: "Edit profile",
          onClick: () => onMenuItemClickRef.current("/profile")
        },
        { label: "Logout", onClick: () => console.log("Logging out") }
      ]}
    />
  );
};

Would it also make sense to replace:

const onMenuItemClick = urlToNavigate => history.push(urlToNavigate)

with:

const onMenuItemClick = useCallBack(urlToNavigate => history.push(urlToNavigate), [urlToNavigate])

so it changes only when urlToNavigate changes?

sotiristherobot
  • 259
  • 3
  • 10
  • Yes, that would make sense. – Praneeth Paruchuri Sep 10 '19 at 12:31
  • 1
    No that does not work. You can only memorize values that come from the outer scope but `urlToNavigate` is an argument provided to the callback itself. – trixn Sep 10 '19 at 12:32
  • Oh damn. I missed that, I was thinking it was a prop. – Praneeth Paruchuri Sep 10 '19 at 12:38
  • @paruchuri-p `history` is the prop that your callback depends on so you should add that to your dependency list instead. Also see my answer for an example. – trixn Sep 10 '19 at 12:39
  • @trixn I'm coming back to this since I still have one question. Do we really need the useRef here? It seems to me that the useCallback hook would suffice in this scenario? – sotiristherobot Sep 12 '19 at 09:03
  • @sotiristherobot Yes you only need `useCallback` for this. It is actually meant to be used whenever you want to pass callback functions that are created as closures to children. `useRef` is usually used to keep a reference on DOM elements or objects created by third party libraries with imperative api. Also just to mention: `useCallback` is actually just a special case of `useMemo` which can be used to memorize anything that may be passed to children. The point is always to prevent passing re-created props with same values to children as that might trigger unnecessary re-renders. – trixn Sep 12 '19 at 11:32
  • @trixn thank you for the answer, just for completeness and for future help to others I would like to add here that useRef except from keeping reference on DOM elements it can be also used to "mimic" the behavior of instance variables from class components. "Setting" a value to it, does not cause the component to rerender. – sotiristherobot Sep 12 '19 at 12:27

2 Answers2

5

In the link you reference in your question the key is that the children component (the one that receives the callback) is memoized.

In a normal Parent > Children cycle, every time a prop from Parent changes it will re-render and Children too, even if no props from Children changed.

If we want to avoid Children from rendering when non of its prop changed then we can memoize the component (React.memo). Once we do so, we might have a problem if in Parent we create a function callback on every render, because the memoized Children will interpret as a new function and thus will render again.

To avoid this we can use useCallback in Parent, so when children receives the callback it knows if it is new and should render or not.

Using useCallback when the children is not memoized does not help because Children will render anyway if Parent renders.


Regarding using useRef for the function, I am not totally sure but I would say that it might be similar to using a useCallback with an empty array as dependencies.

Alvaro
  • 9,247
  • 8
  • 49
  • 76
1

No that does not work. You can only memorize values that come from the outer scope but urlToNavigate is an argument provided to the callback itself. If you used e.g. a prop passed to the component in the callback you could provide that as a dependency.

Your callback has history as a dependency so you could do

const onMenuItemClick = useCallback(urlToNavigate => history.push(urlToNavigate), [history])

This way the callback wouldn't be recreated on every render but only when the history prop changes.

trixn
  • 15,761
  • 2
  • 38
  • 55
  • You are right, urlToNavigate is not coming from the outer scope, that was my mistake. So to confirm that I have correctly understood the concept, If I don't use the useCallback the result would be that the onMenuItemClick will be created every time the component renders? – sotiristherobot Sep 10 '19 at 12:44
  • @sotiristherobot Yes, any arrow function/closure that you define inside your component will be recreated on every render. `useCallback` just memorizes the last created callback until any of the values provided in the dependency list changes. Technically the closure will still be re-created but it will not be used. Instead the memorized callback will be used. – trixn Sep 10 '19 at 12:49