skip to Main Content

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


  1. 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 your App function again, recreating the variables within your component, including the _formVal variable in a new scope context. Since the function reference returned by useCallback() 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:

    let memoizedHandleSubmit;
    
    function foo(x) {
      let handleSubmit = function() {
        return x++;
      };
      
      memoizedHandleSubmit ??= handleSubmit; // if `memoizedHandleSubmit` doesn't have a value assigned to it yet, assign the value, otherwise leave it
      return memoizedHandleSubmit;
    }
    
    const bar = foo(1);
    console.log(bar()); // 1
    const bar2 = foo(10);
    console.log(bar2()); // 2, not 10

    Here we call the foo function multiple times, each time it is called, a new x variable, and handleSubmit function are created. The memoizeHandleSubmit function will essentially hold the first handleSubmit function that was created within the foo function by the first call to foo, subsequent calls to foo will reuse the previously created handleSubmit function (this is what your useCallback is doing). When the first handleSubmit function is created, the closure it saves is of the scope it’s defined in, that means that the handleSubmit function has access to the variables defined outside of it, such as x. When the foo function is invoked again, a new scope is created with it’s own value of x and a new handleSubmit function is created, but this one is discarded and not used, as instead, the old handleSubmit function is returned. The old handleSubmit function still only knows about the scope it was originally defined in as that’s the closure it "saved", so it only knows about the x value from the original invocation, and so the log prints 2 instead of 10.


    Note that there are multiple ways to solve stale state issues, for this particular case, you most likely want to use [formVal] as your useCallback() dependency rather than [], which will create a new reference to handleSubmit when formVal changes, allowing you to access the updated state value within it.

    Login or Signup to reply.
  2. First, let’s take a look at the two code snippets you mentioned:

    First Code Snippet:

        javascript
    Copy code
    function createIncrementFunction() {
      let count = 0;
      return function increment() {
        count++;
        console.log(count);
      };
    }
    
    const increment1 = createIncrementFunction();
    increment1(); // Output: 1
    increment1(); // Output: 2
    Second Code Snippet:
    
    javascript
    Copy code
    function createIncrementFunction() {
      let count = 0;
      function increment() {
        count++;
        console.log(count);
      }
      return increment;
    }
    
    const increment2 = createIncrementFunction();
    increment2(); // Output: 1
    increment2(); // Output: 2
    

    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.

    Login or Signup to reply.
  3. 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:

    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}
          />
          <MemoizedSuperHeavyComponent 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}
          />
          <MemoizedSuperHeavyComponent onSubmit={handleSubmit} />
        </>
      );
    }
    

    Now, let’s focus on the key difference:

    First Code Snippet (with let _formVal):
    In this snippet, _formVal is declared outside the App component. It is declared using let 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 the App component. When you do _formVal = formVal, you are assigning the value of formVal 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 the App component using const. This means it is a local variable with block-level scope, specific to the App 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 using useCallback, 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 by useCallback. In the second code snippet, _formVal is a local variable, so it’s also accessible within the closure created by useCallback.

    In terms of solving "stale closures," both code snippets should behave similarly because _formVal is captured by reference in both cases. When the handleSubmit 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 the handleSubmit function is called. The difference in variable scope (module-level vs. block-level) doesn’t affect the behavior in this context.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search