I wonder what the fundamental (or any kind, really) difference is between using a variable as a dependency in a useCallback()’d function and passing it to the function as a parameter.
a)
const s = useState(0);
const f = useCallback(() => {
console.log(s);
}, [s]);
f(); // logs current value of s
b)
const s = useState(0);
const f = useCallback((s) => {
console.log(s);
}, []);
f(s); // logs current value of s
I just can’t think of an example where they would be functionally different. (Except that in case b) the function reference doesn’t change even if s does, so that would be preferable…?)
In what case would you need a useCallback() with a non-empty dependency list at all then?
2
Answers
The primary difference between the two is when the variable is known and accessible as compared to when the function is called.
Version A
The dependency values are all known ahead of time and can be simply closed over in callback scope, to be referenced later when the function is called. These values will be specified in the
useCallback
hook’s dependency array.Version B
The "dependency" values might not be known until the function is called and passed them. These values will be passed as function arguments to the memoized callback function returned by the
useCallback
hook.In your very trivial example, yes, there is not much difference between the two and the result is identical. In version A the
s
dependency value is known and currently accessible in scope, so its value is closed over in the callback scope and used when the callback function is called. In version B the value ofs
might not be known until the callback is called and the argument passed to it.When used like in your example, again, not much difference. The primary purpose of the
useCallback
hook is to memoize and provide a stable callback function reference. This is important when that callback function is used in the dependency array of another React hook, e.g.useEffect
, where you don’t want to unnecessarily trigger that hook more often than needed, or passed as props to children components and trigger unnecessary component re-renders.Any time there is an external dependency.
Example:
Used as a stable reference to
useEffect
hook call:Used as a stable callback passed as props:
You can directly go to the section conclusion below and read the summary. Still sequential reading may be more useful.
There may be two distinct things we need to discuss over here:
Arguments in a function
As we know, these are the variables formally declared in a function definition. And the actual values will be passed into the function by calling sites. Formal parameters and actual parameters – the two terms which you might have already come across, are applicable here.
Lexically scoped variables in a function
Every function has access to the variables in its enclosing context. This is quite relevant when it comes to nested functions. It means that a nested function has accessed to the variables in its enclosing function. A nested function object along with a scope of variables from its enclosing function is known by the term closure which you would be familiar, already.
Now let us inspect the below sample code and its output
App.js
Test run results
On loading of the app, it displayed as below:
Observations
We may ask here, why it did not print AB, instead it printed CB ? This question is quite relevant since the expression the function returns is this : stateA + stateB.
It printed C first because the lexically scoped variable stateA now is hidden here by the formal argument stateA, which is with the same name. Therefore stateA inside the nested function received the actual value C passed by the calling site, Child component.
Then it printed B second, it did so since stateB is a lexically scoped variable in this nested function. Therefore it printed B as its value. Please note B was the value of stateB in the enclosing function when this nested function was initially defined by useCallback in the initial render.
Now let us inspect another sample code with its output.
App.js
Test run result
On loading of the app, it displayed the same as above:
However, on clicking the button, it displayed as below:
Observations
As we can see in the code, there is now a button added to do a state change. Now on clicking the button, it printed CBB. Please note that it had printed CB on load of the same app. What does it mean may be self explanatory. On clicking the button, there is a state change occurred in stateB, therefore when the child component rendered second time, it could print the latest state. This is quite expected. However here may be an important aspect to emphasise, during the second render followed by the state change, useCallback returned a newer definition of the nested function. It did so since stateB has been specified as its dependency. It did so only because of this non-empty dependency setting. Let us see below if this statement is true or not.
Let us see the below code which ignores the linter advice and removed the dependency to make it empty.
App.js
Test run results
On loading the app, it remains the same as in previous two cases.
On clicking the button this time, it still prints initial stateB.
Observations
What could have been happened here ? A simple guess would tell us that the stateB in the nested function has not been found the latest state value. Therefore it still prints its initial value. The reason may also be self explanatory. The useCallback did not redefine the function in the second render since it has not been told to do so through its dependency.
If we still think that we would have defined the function with two formal arguments stateA and StateB and could have avoided this failure. And we then could have still left the dependency array blank. However, this is not the same compared to accessing lexically scoped variable. Please see the conclusion below.
Therefore the conclusion may be like this :
We need to have both – formal arguments and lexically scoped variables, in a nested function to meet the programming requirements. We cannot replace lexically scoped variables with formal arguments. It is because the value of a formal variable is passed in by the calling site. More specifically, the value of a formal variable is passed in when it is invoked. However, through lexical scoping, a nested function can access the value of a variable at the time the function was defined and not at the time it was invoked.
This may be the most important point :
In order to get the latest values of lexically scoped variables, the nested function definition has to be redefined with respect to its dependency changes. This is the technical reasoning for specifying a non-empty dependency array.