in React, when a component re-renders, all the functions inside that component will be recreated, resulting in a change in function references.

So that even if a child component is wrapped in memo() to prevent it from unnessecary rerenders caused by parent component. It would still rerender in case a function is passed to it as a prop from the parent component?

Here is an example:

const Parent = () => { 
  const [state, setState] = useState("1"); 
 
  const handleIncrement = useCallback(() => { 
    const increment = setState(state + 1);  
  }, [state]);
 
  return (  
    <Child handle={() => handleIncrement()} /> 
  );
}

const Child = memo(({handle}) => {
  return();
});

That's the main purpose of using useCallback() on functions passed to the child component? so that the function's reference will stay the same between rerenders that don't affect the usecallback dependency array for that function in the parent component. Which passes the reference equality check done by the child component?

Resulting in maintaining the same function reference when a function is passed as a prop to a child component? preventing rerenders from making the use of memo() uneffective?

My question: is that the most important use of usecallback(), generally?

1

There are 1 answers

5
Greg Fenton On BEST ANSWER

A "component" is simply a JavaScript function. "Rendering" a component simply means "running the function".

You are correct that each time a JS function runs it recreates the variables within it, including function definitions.

The "magic" that React offers to avoid things looking like they are being recreated is useState(), useMemo(), useCallback(), etc. These are in no way bypassing the base JS behaviours mentioned above. These hooks "cache" values when they are first run, and subsequent runs (i.e. subsequent "renders"), if React sees that it has a variable/object/function in cache then it gives that value back to the "new" instance create by useState/useMemo/useFunction as was in the previous run.

Now, in your above example if the handle property changes then it will regenerate the Child component and so it will be re-rendered. You have at least 2 ways to avoid this:

  1. use useCallback() when defining handleIncrement() (as you have done....but see below)
  2. pass a second parameter to memo() -- a function that is meant to compare previous and current props to the component and return true if they are the same (i.e. do not render). So you could simply set that second parameter to () => true and it will never re-generate and never re-render.

Now ...as for your current handleIncrement(). You have it using a useCallback() that is dependent on the value of state. But each time that handleIncrement() is called, it is increasing the value of state. So that will cause Parent to re-render and with the new value of state useCallback() will re-generate handleIncrement() as a new function because its dependency has a new value.

And....one last thing: setState(state + 1); is a TERRIBLE pattern to code to. Because setState() is an asynchronous function, you are telling React (well, JavaScript): "at some point later in time, please take the current value of state, add 1 to it, and put it back into state".

The problem here is that if setState() gets called multiple times before JavaScript gets around to running its asynchronous queue....you can end up losing data!!!! If the current value of state is 7 and you code:

setState(state + 1);
setState(state + 100);
setState(state - 2);

then when your component finally gets around to re-rendering the value of state will be 5...because JS was told to run 7 + 1 and 7 + 100 and 7 - 2...and the last one wins.

Whenever you have a state variable and you want to update its value to be relative to its current value -- whether that be an array, an object, a string, an integer, whatever -- then you want to use the "callback signature" of the "set state function": setState(currVal => currVal + 1).

This syntax tells JavaScript "at the timet that you are running the set state function, get its current value, add 1, then put it back into the state variable".

With this latter approach, if you call setState() a thousand times, it will get the correct answer every single time. Given the same conditions above, state is 7 and you code:

setState(currVal => currVal + 1);
setState(currVal => currVal + 100);
setState(currVal => currVal - 2);

then JS will do: 7 + 1 and 8 + 100 and 108 - 2. The value on re-render will be 106. The main difference is that setState(currVal => ....) gets the value of the state variable at the time that setState() is running, whereas setState(state + 1) gets the value of state ***at the time setState() is being added to the asynchronous queue.

If you haven't already, I strongly encourage all of my friends (and make it a requirement of all of my coworkers) to watch the video JavaScript: Understanding the Weird Parts multiple times, especially the parts about Synchronous and Asynchronous code execution (time links are in the video description).