I’ll admit it straightforward- I sometimes enter stackoverflow and look at the questions posted; I just enjoy to give back to the community my humble share back. 😇
Sometimes, a question in stackoverflow rises from the deep to shed some light on a subject in programming that I know well, or at least thought I knew well - and makes me second guess my knowledge and understanding of the subject.

Recently, a question of this kind caught my eye; in essence, it seems that the enquirer was trying to implement some react-esque useEffect hook, but he stumbled across trouble with the famous JavaScript scope.


The JavaScript scope

But what is the JavaScript scope? Quoting from the holy bible of the web developer, the MDN:

[The scope is] The current context of execution. The context in which values and expressions are “visible” or can be referenced. If a variable or other expression is not “in the current scope,” then it is unavailable for use. Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.

Furthermore, in other parts of the program, the same variable name may refer to a different entity (a different binding), or to nothing (unbound). 1

Armed with this knowledge, let’s look at the code in question:

When run, it prints:

"before", {
  age: 0
}
"after", {
  age: 0
}
"replicaUseEffect", {
  age: 0
}

At this point, some of my JavaScript alarm bells started to ring 🔔 - why does after() print {age: 0}? Didn’t we increase the value?

Furthermore, when appending another call to foo(), it results in this:

"before", {
  age: 0
}
"after", {
  age: 0
}
"before", {
  age: 1
}
"after", {
  age: 1
}
"replicaUseEffect", {
  age: 1
}
"replicaUseEffect", {
  age: 1
}

Which seems rather odd - the second call to replicaUseEffect() prints the same {age: 1} value! 🔔

Let’s take a closer look at this code, and read it line-by-line:

let state = {
    age: 0
};
let getState = () => {
     let setState = () => {
        state = {
            age: state.age + 1
        }
    }
    return {
        state,
        setState
    }
}

function foo() {
    let {
        state:st,
        setState
    } = getState();
    replicaUseEffect = () => {
        console.log("replicaUseEffect", st);
    }
    console.log("before", st);

    setState()
    console.log("after", st);


    setTimeout(() => replicaUseEffect(), 1000)
};
foo();
foo();

Well, the first three lines define a global state variable (using let, of course, so we could reassign to it).
Next, we have the function getState. This function defined a setState function that reassigns state with an incremented by one age.

Aha! This solves my first mystery alarm bell 🔔: why does after() print {age: 0}? Didn’t we increase the value?
Indeed, we increased the value, but we also reassigned state. Remember - foo() sees a reference to the old state variable.
If we had run getState(); again before logging state again, we would have gotten the updated value.

But the mystery of the setTimeout call remains. To solve it, let’s continue our quest.

We have arrived at the function foo().
This function calls getState() and reassigns its results as {state:st, setState}.
Then, a function named replicaUseEffect is defined as a simple console.log of the st value.
Note that replicaUseEffect is defined without const or let.
foo() then prints st, calls setState() (which reassigns to the global state, remember?), and then prints again st (which wasn’t changed).
Then it calls setTimeout with replicaUseEffect and a delay of one second.

Finally, we call foo() twice. It seems that second time we run replicaUseEffect(), it somehow makes the first invocation log the second value of st:

"replicaUseEffect", {
  age: 1
}
"replicaUseEffect", {
  age: 1
}

The JavaScript event loop

Of course, that is not true - there’s no way that we could make our first invocation to “peek” into the future and see the next value of st… unless we make it so.
In order to understand what’s happening here, we should remember that by calling setTimeout, we “push” our function to the message queue2: we’re telling js that we want to run this code after the event loop has finished evaluating the current function; the code is called from an execution context separate from the function from which setTimeout was called. 3

Now, before we untangle this mess, note that when we call foo(), we are actually reassigning replicaUseEffect:

replicaUseEffect = () => {
    console.log("replicaUseEffect", st);
}

By not using let, const, or var, we’ve defined replicaUseEffect on the global object (in the browser, it is the window object). Then, we reassign to it, so when replicaUseEffect - the callback of setTimeout - is actually called (at least 1 second since our function has finished executing), it is replaced by another version of it - because we reassigned it.
And although its code is exactly the same, it lives in a different closure, which has the st local variable of the second execution context of foo.

When replicaUseEffect is defined as a local variable, this problem vanishes:

let state = {
    age: 0
};
let getState = () => {
     let setState = () => {
        state = {
            age: state.age + 1
        }
    }
    return {
        state,
        setState
    }
}

function foo() {
    let {
        state:st,
        setState
    } = getState();
    const replicaUseEffect = () => {
        console.log("replicaUseEffect", st);
    }
    console.log("before", st);

    setState()
    console.log("after", st);


    setTimeout(() => replicaUseEffect(), 1000)
};
foo();
foo();

This indeed prints {age: 0} and then {age: 1}:

"before", {
  age: 0
}
"after", {
  age: 0
}
"before", {
  age: 1
}
"after", {
  age: 1
}
"replicaUseEffect", {
  age: 0
}
"replicaUseEffect", {
  age: 1
}

To sum it up, this post talked about some very delicate subjects in js: the event loop and the scope.
Indeed, the scope can be intimidating and complicated at first sight - which, with this question in mind, reminds us to use let and const. The event loop also has its proper place in the list of things that are unclear at first when starting to code in js.

I encourage everyone who enjoyed this article to read some more about the event loop and the scope in mdn.
Also, this video on the js event loop cleared my mind a bit - maybe it could the same to you, who knows

  1. This is based on wikipedia 

  2. While this is based on mdn 

  3. And this is also based on mdn 

Comments