Photo by Almos Bechtold on Unsplash

There’s No Magic in React

The fundamentals of JavaScript that every React developer should know to reveal the tricks underneath!

Leonardo Di Vittorio
JavaScript in Plain English
8 min readOct 27, 2022

--

The title may seem obvious, but in my experience teaching coding, I’ve encountered developers who, when they don’t understand what’s happening, believe there’s some sort of dark magic happening.

Magic

The aim of this text is to gain a deeper understanding of what occurs within components when we update their state. To do this, we’ll step back and revisit some fundamental concepts of “Vanilla JavaScript” before exploring how these concepts relate to the flow of React.

Agenda

  • Recap of what are Scope and Closures in JavaScript.
  • How these two concepts affect our flows in React (with useState).
  • Difference between updating the state with a value and with the updater function.
  • Recap of object’s references in JavaScript.
  • How to deal with references within React.

Do you remember what’s Scope?

Scope is a set of environments. Kyle Simpson describes them as a collection of buckets, where we can store our declaration and values. Then, at execution time, we will be able to access them from inside the same bucket.

JavaScipt scope

The example above works in this way: The JS runtime engine will go through our code starting from the “global scope”, where we have only one declaration, which is count. Then, when we call count for the first time, the JS engine initializes a new scope we can call “the scope of count”. For JavaScript, this is a whole new dimension. Here we have different variables and declarations, accessible just from the inside of the scope, not from the outside. Once we’re done and the function returns, this environment will be trashed. Here we declare n equal to 0 and sum to it 1 and finally, log its value.

What happens if we call count again? Obviously, is going to print again 1 because new calls will always set a new scope and declare again let n = 0.

To insist on this concept of scope, if we try to log n on the last line, we will get undefined because n exists just in “the scope of count” and we can’t access it.

So how do we get to have a variable that updates every time we call the function?

Following the concept that: “each scope can access its outer scopes”, we can shift up nof one scope so when we call count we will always refer to the same declaration and we keep updating its value, so in the last line, we’re going to print 2.

JavaScipt scope 2

But it’s very inconvenient to pollute the global scope of several variables, no?

And what about Closures?

Closures would perfectly solve the issue. Here you can find a definition of what a Closure is:

“A Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.”

In the code below, we are able to call the functioncounter, which will return another function, which we assign to count. Every time we call count we access to n from the outer scope (line 2) and keep updating its value, and everything will happen in isolation without polluting the global scope.

Example of Closure

Where does the state update?

Going forward and moving to React, but keeping in mind what we said before: “A declaration exists only in its scope”.
Why are we surprised to see that in this code it still logs 0?

Pseudo code for useState Hook

In this pseudo implementation of a useState hook, we see that we create the state and return it together with a function to update it. The concept is similar to what we just saw. Whenever we call the setState function to update the state we’re actually changing the value of the variable inside the scope of useState, which is completely different from the scope of Component. When we log state in the last line we’re still referring to the value assigned inside Component which still is 0.

I want to ask a question now: What’s the main difference to have hooks?

Before, the functions Components were used to render presentationals, components that didn’t have a state but used their incoming props to render. So they were stateless, right?

The main difference with having, in my opinion, is that: Now, we can keep the state in between function calls. As we said at the beginning, each function call initializes a new scope and holds just its own values. In the following code, we can see how we’ll call Component again and it will receive the latest value from the useState hook, which is always accessing the same declaration, and this time it will be 1.

Multiple Component function calls

It’s crucial to understand that every render is a new function call, and each function call sets its new scope with declarations and values.

States update with value or updater

Now, can you tell me what will be shown on the screen with this code?

Counter component with setInterval

What’s your guess? (Spoilers ahead…)

Once you gave your answer open this sandbox to see if you were right and to play around, implementing a solution for your counter. If you need a hint keep reading.

Take a look at this pseudo version of the useState hook (you can find the code in the useState.js file in the sandbox) and keeping in mind what we said so far about Closures, try to follow the flow line by line.

Pseudo useState implementation

So let me guide you throw this. First, it calls useState and initializes the state to be 0 which is returned in form of a tuple with the function to update it. Now, in the “scope of useState” and in the “scope of Counter” both_state and count are 0.
Then we call the useEffect one time, because of the empty array of dependencies. This will set an interval that every second will call setCount passing count. Here is the important part. Setting the interval we’re declaring a new anonymous function in the first “scope of Counter” as callback. Every time we will call this callback, it will look for the value of count inside that same scope, where count will be always 0. When calling setCount we’re actually doing the following:

setCount(0 + 1) // => 1

This is why we will always see 1 on screen, even though the interval is running.

How can we solve it? We should be able to access the most updated value of count, the one contained inside the scope of useState to not rely on the one in “the scope of counter”. Luckily the useState hook gives us the possibility to pass an updater function that gets as an argument the last computed value.

setCount((count) => count + 1)

This gives us the guarantee that we will sum 1 to the latest value and our counter will now work properly.

Primitives and References

Another big source of confusion among JS developers is the concept of primitives vs objects and all the consequences that derive from there.
In React we should have a good understanding of this topic to make sure we don’t stumble into annoying mistakes, like infinite loops. I think everybody can guess where I’m going with this.

Just to recap. Even though JS is not strictly-typed it does have different elements that allow us to make specific operations, and these are defined by their types.

We have primitives, which are everything but objects, arrays, and functions. These last three fall under the name of “objects”. Objects have the peculiarity that, when we initialize one, we allocate space in memory to save the value and we save the reference to that space in the current scope. We can pass around the reference but we always refer to the same object in memory.

With this said, when we make a comparison between objects, we compare their references, meaning that they are exactly the same space in memory, and not that they have the same properties and values.

Here is an example of some comparisons:

Values comparison

References in React

Talking about React, in fact, when we have, for example, a custom hook that returns a function or an object, we need to be careful and make sure that we use them correctly to avoid useless renders or even infinite loops.

Take a look at the next example. In the component SessionTime we take advantage of the useCounter hook to get the time passed since the beginning of the session, and the function to reset it. Every second we call useCounter and we re-declare resrt and time every time with new references. This would lead to having an infinite loop inside SessionTime as the useEffect has resert in the array of dependencies.

Important: The array of dependencies is a list of elements that React Hooks take to understand when they need to run again. Internally they do a swallow comparison deps[i] === deps[i], and if it’s false it runs the hook.
If we go back to the comparisons code above we can see that with primitives this would be fine, but with objects, we need to be careful because it’s not enough that they look the same,
they need to be the same.

useCounter hook returns always new references

The solution here is to use memoization so, unless we need to update them, we will always keep the same reference. In this case, we can wrap reset into a useCallback and we’d fix the problem.

Keep the same references memoizing the values where possible

Notice that time is still creating a new reference? Well, when we use memoization we need to consider case by case. Here, time uses to count, which updates every second, thus wrapping it into a useMemo would not change the implementation, but would just occupy more space in memory without anything in return. Let’s say in this case it’s expected.

Going back to our closures, keep in mind that, with these types of hooks where we use callbacks, we always create a closure that will use the values inside its scope, so we need to make sure to pass the right dependencies. In any case, React makes a good job of pointing out when and what we should add to the dependency array with linter errors.

Takeaways

The main goal of this article is to understand that everything that happens inside our components and hooks is just JavaScript and once we understand that, there will be magic no more.

Let’s recap:

  • Scope is a collection of environments where we save variables and declarations, which can be accessed by the same scope or inners scopes only.
  • Closure are functions that make those scopes accessible and allow us to keep memory of the values.
  • In React, every render is a new function call, and each function call sets its new scope with new variables and declarations.
  • When we updated the state with setState we’re actually updating the value inside the useState hook and not inside the component. The component will receive the new update value in the next call.
  • When we call setState if we want to be sure to use the latest value we can use the updater setState(last => last + new)
  • When we compare objects, functions, and arrays we compare their reference (the address in memory where the element is saved), and not if they have the same properties and values.
  • When we pass dependencies to the hooks, we need to make sure we’re not passing a newer reference every time by memoizing their declarations.

More content at PlainEnglish.io. Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord. Interested in Growth Hacking? Check out Circuit.

--

--