It feels like there is nothing difference between the first one and the second one. But the first one code snippet can solve the stale closure. So, why the second one can’t? I really can’t figure it out. Can anyone explain it from the principle of JavaScript closures?
// ========================== First code snippet ========================
let _formVal
export default function App() {
const [formVal, setFormVal] = useState('');
_formVal = formVal
const handleSubmit = useCallback(() => {
console.log('_formVal:', _formVal);
}, []);
return (
<>
<input
onChange={(e) => {
setFormVal(e.target.value);
}}
value={formVal}
/>
<MemoziedSuperHeavyComponnent onSubmit={handleSubmit} />
</>
);
}
// ========================== Second code snippet ========================
export default function App() {
const [formVal, setFormVal] = useState('');
const _formVal = formVal
const handleSubmit = useCallback(() => {
console.log('_formVal:', _formVal);
}, []);
return (
<>
<input
onChange={(e) => {
setFormVal(e.target.value);
}}
value={formVal}
/>
<MemoziedSuperHeavyComponnent onSubmit={handleSubmit} />
</>
);
}
3
Answers
In the first example, there’s only ever one
_formVal
variable created, which sits at the top module-scope level, so that’s the variable your function component reads and updates.In your second example, multiple
_formVal
variables are created for each render of your component, as each rerender calls yourApp
function again, recreating the variables within your component, including the_formVal
variable in a new scope context. Since the function reference returned byuseCallback()
never changes and is always the first function created on the initial render (due to the empty dependency[]
), the function will always access to the first_formVal
variable that was created in its surrounding scope and not the subsequent ones from future renders created in other scopes.Small example with pure JavaScript:
Here we call the
foo
function multiple times, each time it is called, a newx
variable, andhandleSubmit
function are created. ThememoizeHandleSubmit
function will essentially hold the firsthandleSubmit
function that was created within thefoo
function by the first call tofoo
, subsequent calls tofoo
will reuse the previously createdhandleSubmit
function (this is what youruseCallback
is doing). When the firsthandleSubmit
function is created, the closure it saves is of the scope it’s defined in, that means that thehandleSubmit
function has access to the variables defined outside of it, such asx
. When thefoo
function is invoked again, a new scope is created with it’s own value ofx
and a newhandleSubmit
function is created, but this one is discarded and not used, as instead, the oldhandleSubmit
function is returned. The oldhandleSubmit
function still only knows about the scope it was originally defined in as that’s the closure it "saved", so it only knows about thex
value from the original invocation, and so the log prints2
instead of10
.Note that there are multiple ways to solve stale state issues, for this particular case, you most likely want to use
[formVal]
as youruseCallback()
dependency rather than[]
, which will create a new reference tohandleSubmit
whenformVal
changes, allowing you to access the updated state value within it.First, let’s take a look at the two code snippets you mentioned:
First Code Snippet:
At first glance, these two code snippets might seem very similar. They both define a function createIncrementFunction that returns an inner function increment which increments a count variable. When you call the returned inner function, it increments and logs the count.
However, the difference lies in the way closures work in JavaScript and how they capture variables from their surrounding lexical scope.
In the first code snippet, an arrow function is used as the inner function (increment). Arrow functions inherit the scope of their containing function (createIncrementFunction in this case). This means that when the inner function (increment1) is returned and executed outside of createIncrementFunction, it still has access to the count variable through the closure. This prevents the "stale closure" issue.
In the second code snippet, a regular function is used as the inner function (increment). Regular functions create their own scope, and they close over variables declared within their body, not the surrounding lexical scope. When the inner function (increment2) is returned and executed outside of createIncrementFunction, it doesn’t have access to the count variable declared within createIncrementFunction. This can lead to a "stale closure" issue where the count variable cannot be accessed or modified outside the original scope of createIncrementFunction.
In essence, the first code snippet uses an arrow function for the inner function, allowing it to retain access to the surrounding scope (closure), while the second code snippet uses a regular function that doesn’t capture the same closure, causing the "stale closure" problem.
So, the key takeaway here is that the choice of function type (arrow function vs. regular function) used within closures can significantly impact their behavior and their ability to access variables from their enclosing scopes.
In the two code snippets you provided, both seem quite similar at first glance, but they do have a subtle difference, which relates to how closures work in JavaScript. Let’s break down the differences:
First Code Snippet:
Second Code Snippet:
Now, let’s focus on the key difference:
First Code Snippet (with
let _formVal
):In this snippet,
_formVal
is declared outside theApp
component. It is declared usinglet
at the top of the module, making it a variable with module-level scope. This means it can be accessed and modified from anywhere within the module, including inside theApp
component. When you do_formVal = formVal
, you are assigning the value offormVal
from the component’s state to_formVal
in the module’s scope. This creates a reference to the same variable.Second Code Snippet (with
const _formVal
):In this snippet,
_formVal
is declared inside theApp
component usingconst
. This means it is a local variable with block-level scope, specific to theApp
component’s function. It is not accessible outside the component.Now, let’s discuss the implications of these differences in terms of closures:
In both cases, when the
handleSubmit
function is created usinguseCallback
, it captures variables from its surrounding scope to form a closure. In the first code snippet,_formVal
is a module-level variable, so it’s accessible within the closure created byuseCallback
. In the second code snippet,_formVal
is a local variable, so it’s also accessible within the closure created byuseCallback
.In terms of solving "stale closures," both code snippets should behave similarly because
_formVal
is captured by reference in both cases. When thehandleSubmit
function is executed, it will always log the current value of_formVal
.So, the reason why both snippets work similarly and can solve the "stale closure" problem is that
_formVal
is captured as a reference in both cases, ensuring that the correct and up-to-date value is used when thehandleSubmit
function is called. The difference in variable scope (module-level vs. block-level) doesn’t affect the behavior in this context.