0

I have stumbled upon an issue that is very weird for me, but most probably is very simple to explain.

Demo

Let's assume the following React component

import React, { useState, useEffect, useCallback } from "react";
export default function App() {
  const [test, setTest] = useState();

  const doSomething = () => {
    // TODO: Why does this returns the inital state value? Hoisting?
    console.log(test);
  };
  const doSomethingWithCallback = useCallback(doSomething, [test]);

  useEffect(() => {
    setTest("asas");

    window.setTimeout(() => doSomething(), 2000);
    document.addEventListener("click", doSomethingWithCallback);

    return () => {
      document.removeEventListener("click", doSomethingWithCallback);
    };
  }, [doSomethingWithCallback]);

  return (
    <div className="App">
      <h1>Click anywhere</h1>
    </div>
  );
}

(cf. CodeSandbox)

Question

Look at the TODO comment in the code.

Why does doSomething console logs the state test as it is initially is set, namely undefined whereas the callback variant is returning it with the "real" current state when it is called?

Is this some kind of hoisting or performance optimization React is doing?

andi1984
  • 676
  • 10
  • 27
  • 1
    No hoisting involved here - you declared `const test` at the top of your function. It's just a normal closure over that constant. – Bergi Dec 08 '20 at 18:37
  • 1
    Notice that `setTest("asas")` does not reassign your constant. It just causes a re-render with a new state value. – Bergi Dec 08 '20 at 18:38

1 Answers1

0

This doesn't have much of anything to do with hoisting. setTest is causing a rerender which will cause the useCallback to evaluate it's callback since it's dependency changed.

Explaining why the other function call returns the initial value is a little more mysterious, but I think it has to do with the value of test when you registered the call to setTimeout.

Any setState is asynchronous, you can not guarantee the value in state will line up with what you expect if you set the state and then synchronously try to reference state right afterwards. To the contrary, it most likely won't, I'm not fully informed, but I believe more or less no asynchronous work is done until the queue of synchronous work is completely empty.

As for your actual question, "are state values hoisted?" yes they are, like all values in Javascript. However react state hook values differ slightly in that the order of their execution must always be the same between renders, so you have to maintain at least that much (i.e. no conditional hook instantiation may exist).

Cory Harper
  • 1,023
  • 6
  • 14
  • You are incorrect. Hoisting happens within a certain scope for literally any kind of variable: https://stackoverflow.com/a/31222689/11753100 – Cory Harper Dec 08 '20 at 19:06
  • Incorrect. Read the post I linked, and please do not bother me about this again. let/const are hoisted, but can not be accessed before they are initialized. Hoisting and your ability access said variable before initialization are, in concept, two entirely different things. Hoisting is your ability to referencing a variable before it was declared in the code, and still be referencing the same thing. – Cory Harper Dec 08 '20 at 19:12
  • Please don't tell me what or what I cannot do. Thankyou.. – Keith Dec 08 '20 at 19:17
  • I asked you to please not bother me with misinformation, that's hardly telling you what to do. ```js const letsHoist = () => { const proofOfHoisting = () => x; const x = 1; return proofOfHoisting(); } ``` – Cory Harper Dec 08 '20 at 19:21
  • Yes, I forgot about the temporal dead zone stuff,. I still think it's a kind of hoisting,. as the exceptions proves. But technically yes, it's hoisted. Sorry for the miss information. – Keith Dec 08 '20 at 19:26
  • So, just to wrap it up from what I understood: Hoisting is not what's happening here, since in the code we do not try to use a variable that is declared afterwards, but rather it is the closure that basically binds the variable's value at the time we define the `doSomething` function to it, means `undefined`. Thus React's mechanisms like `useCallback` or potentially using the setState`s callback function are ways to redeclare those methods under the scope of the updated state value. Is that a correct summary? – andi1984 Dec 08 '20 at 20:01
  • 1
    That's pretty accurate, yes. `useCallback`, `useMemo`, and `useEffect` are designed to work closely with a React component's lifecycle and rerender process. – Cory Harper Dec 08 '20 at 20:07