There’s No Magic in React
The fundamentals of JavaScript that every React developer should know to reveal the tricks underneath!
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.
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.
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 n
of 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
.
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.
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
?
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
.
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?
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.
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:
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.
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.
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 theuseState
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 updatersetState(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.