useState — Diving Deep into React Hooks (2021)

Ricardo Pedroni
RPEDRONI
Published in
12 min readFeb 23, 2021

Welcome to our Deep Dive Series on React Hooks! In each lesson, we’ll look into each hook offered by React at the time of this writing (who knows what the future holds 🔮). We’ll go beyond their basic usage (but cover that too!) to understand why each one exists, what is the best scenario to use each and how we can squeeze every ounce of hook-goodness they can offer.

Little known fact: Jack Sparrow is a proficient frontend developer, but still doesn’t use hooks.

In our first session, we’re going to take a look at the simplest but often most useful of all hooks, useState .

The Way Things Were

Before even talking about hooks, let’s take a look back at how React handled things before hooks existed. Formally speaking, let’s take a walk back to React pre version 16.8, where functional components were a bit rare and class components ruled the frontend jungle.

This simple component illustrates how we worked with class components and, specifically, how we handled state back then. React components enforced state control using its infamous this.state and this.setState(), which, when used together, handled the reading and updating of state, respectively. Since components were classes and instances of classes, there wasn’t much debate as to how state was conserved — class instances inherently hold state in memory, that being one of the core principles of OOP and classes. As a developer, you could sleep assured that you understood how your component’s state was handled and where it was “saved”.

Nothing like a good night of sleep knowing which RAM block my data lives in.

But once the React community started noticing some issues with class components (in special regards to reusability and maintenance), React decided to shift to and embrace functional components completely, meaning that components were not classes anymore, they were functions.

Let’s be very clear here: this was an excellent decision which I’ve never heard a single developer complain about (if you’re that developer, please comment). But with this change in scenery, what was once a trivial and intrinsic thing of saving and updating state in class instances, became a challenge. After all, how do you hold state in something inherently stateless like a function?

To that end, let’s exemplify a functional component holding (or attempting to hold) state, as before:

Functional components are much cleaner, showcase better what the components intend to do and render and are overall much easier to write and maintain — props to the development team for this shift in paradigm. All that being said, here we’re are trying to hold state by using a variable, declared in let count = 0; with an initial value. We also have an update function passed to onClick that increments count by 1 on every click. With all this done, let’s run this and see the magic happen!

…or not. If we run this, even if we click the button 50 times, nothing happens. No matter how many times clicked, the component still renders a flat 0 onscreen.

Wait, I know! The problem is obvious — since let count = 0; is inside the function component, so every time the component renders, since this is just a function, count will be set to 0 and that’s why we don’t see anything change. Let’s move our declaration outside of the component and that should fix things:

And there you go! Just click that sweet button and see..

…nothing happen again. There something strange here, it’s not possible that count doesn’t update, is it? We put a console log in our updater to see at least what value count contains through the multiple clicks:

So count‘s value is actually updating, but why isn’t React or our component picking this up?

One important thing we need to remember from “classic” React and class components is that state updates were never done directly to the values saved on state — that is, while we read state variables normally, using this.state.myValue, you should never update state by trying to change that variable directly (this.state.myValue = 10). If we wanted to update a state, we always used this.setState, passing whatever variables needed updating. And the reasoning for this is simple: though some value changed, React doesn’t “monitor” values directly, instead being reactful of changes it’s actually aware of.

React is great at controlling updates and render cycles but we need to signal React that we actually intend on updating something so it can pick up this “attempted” change (I put “attempted” in quotes since not every change necessarily implicates in a rerender — we’ll talk about this later on). That being said, when we talk about state management in React, we’re actually discussing two saving state AND signaling we want to do an update. Since React covers part of this, we need some tool that plugs in to React to do it all.

For functional components and the future of React, the way to do this using hooks — in our case, useState

Let’s see how this baby works:

useState returns an array, where position [0] is the actual value were maintaining in state and position [1] is the state update function, typically called setState (though it’s name is usually always the name of the variable preceded by set, such as setCount). Reinforcing, we should never update state by trying to alter the state value directly (i.e. count = 5) since we have an update function for this (and count is a const anyway).

Using the setXXX function returned (we’ll call this setState from now on, independent of what it’s actual name is) we’re able to update our state variable in a similar fashion we did with this.setState from class components. Let’s see useState in a (possibly) real component, demonstrating some of it’s abilities:

First thing to notice is that useState accepts an argument. This argument, which can be an arbitrary value or a function (we’ll talk about this function in depth later on) is used to give the state variable its initial value. In our previous example, we’re actually using a value from a passed in prop. The same applies to setState function, which equally accepts an arbitrary value (string, objects, etc.) or a function. Passing it a function is useful when we want to update the state with a previous value of itself, such is the case with our prevCount => prevCount + 1 example. Here we don’t need to access the count value directly since this callback will receive the current (“previous value”) and it should return the intended new value.

If you came from good ol’ React with class components, you’re probably used to updating state using this.setState and benefiting from React’s automatic state object merging. So in this example,

even if I called this.setState only passing in a value for count, the component would what it currently has in memory with what was passed to the function, and would have count updated and x still with 10. This is not the case with useState, since any call to its returned setState will completely override whatever was saved there previously.

Here, we’ll initially have an X: 10 displayed in our component but once the button is clicked, setState will overwrite the previous { count: 0, x: 10 } object with the new object passed to it, in this case simply { count: state.count + 1 } .

Due to this, useState is usually used for each data that needs to be in state, having multiple calls to it for each of these pieces.

One logical conclusion you may have come to is that useState isn’t a very adequate solution for keeping complex and/or big data structures, like large objects or deeply nested structures (here’s a gold sticker for your observation ⭐️). That being said, React and its community have multiple options for doing specifically this, through other hooks (e.g. useReducer ) and libraries (you name it — redux, recoil, mobx, etc. etc. etc.). These solutions come with the benefit of better separation between UI and state management, but with the downside of making your code a bit harder to read and some necessary boilerplate to set it all up — but we’ll talk about this when the time comes ✨

Render For Me…

We’re mainly talking about state here but don’t be fooled — part of React’s large appeal to its users is its rendering capabilities, or better yet, its ability to not render that much. Remember that a big part of a FE developers responsibility is that whatever interface we’re building, it should be snappy, load fast and respond quickly to user interactions (the rule of thumb here is to maintain 60 frames per second). That being so, we should strive to keep this performance and React helps us quite a bit in this department.

⚠️ Fair warning: We’re discussing optimization and performance and while having this in mind is always healthy, deeper and more time consuming optimization should never be your first task when developing an application.

One thing to note is that React is pretty clever when it comes to rerendering components, since it knows to avoid rerenders when the updated state value is the same as its previous value. Contrary to what many believe, simply calling setState does not force a render if React notices a render isn’t needed.

For instance, if we look at our previous examples of using a count state variable and we make an update from value0 to 1, React will be aware of this change and rerender the component accordingly. Suppose now that we create a reset button, which sets the count value to 0 . If we press this button multiple times, will React rerender our component on every click? No, React knows that a render isn’t necessary since no value changed and updating the screen isn’t necessary. Pretty smart, right?

…But Don’t Render That Much

It really is pretty smart. But, well, it’s not thaaaaat smart. What React does, in a simplified manner, is to check if the new state value is equal to the current state value. The equality check React does is using the triple equal === (i.e. strict equality), checking if there is a change newValue === currentValue . If not, no rerender is needed, and that’s good 👍

But coming back to using useState to hold objects, we can encounter a nice ol’ source of problems when using it haphazardly.

In this case, we have a very simple component that hold a user instance and updates it when the Reset User button is clicked — while actually updating it to the same exact value every time. We put a console.log to help us see this during its life time. Let’s try this out by clicking the button a few times.

A few 35 times, just to make our point.

Heck, there goes our “React is smart” statement. So why the hell does our component rerender if it never changes in terms of value or visually? The answer is that React is pretty smart, but it requires your help too.

We previously mentioned that React triple equals === values to detect if a value is new or not, rerendering when it detects a change in this comparison. Well, that continues to be true, even if what we’re comparing is a string, a number or an object. In this case, it’s an object (remember that in JS land, anything that isn’t a primitive is an object) and object strict comparisons are shallow, in the sense that they don’t compare an object’s values but instead the reference to that object. So while we can have const a = {}; const b = {}; , the comparison a === b is false since these are variables referencing different objects.

In our example, while the values of the passed object were the same, on every update it is referencing a new, different object, so React just goes and rerenders the component anyway, oblivious to the fact — after all it’s your fault you’re using useState for objects 😜

Classic React had its shouldComponentUpdate() method to help in these cases but we are long gone from the class component world. Now, there are several ways to solve this, be it by breaking up the object by it’s fields (in our s̶t̶u̶p̶i̶d̶ simple example, we could’ve just had a username state variable directly) or using a more apt tool for this, such as useReducer that we mentioned before or doing some type of memoization — but we’ll also talk about these in the not-so-distant future.

Wake Up, Lazy State

There are cases where we want to initialize a component’s state with a given computation, such as the result an expensive function. We can see such a case down below:

Using your imagination (please do), imagine the getValue is a function that produces a (useful) value after doing some heavy computation. We need this heavily computed value to initialize our count variable with its first value — nothing to special here.

If you play around with this component, you’ll notice a couple of things (besides the stupidest way of obtaining the constant value 1000000000 ). First off, when the component loads, there is a significant delay to actually render the component (or if you’re on one of those fabled quantum computers, increase the number of 0 in the loop). One could imagine that this would make sense for the first render (aka the mount) of the component, since it has to do the heavy calculation to get the initial value. But what is quite strange is that, if we click the button to increment count , we notice this same delay every single time. This begs the question: why is our expensive calculation being done every single render if we only need it for the initial value?

The answer is pretty simple: React components are nothing but regular functions. If a function calls another function inside of it, and this call isn’t conditional (meaning it will be called on every function invocation —in our case, it has to be since React hooks are prohibited from being called conditionally), it will execute the whole body of that function, independently if we use that value for something useful or not. And here this is an obvious big no-no. So how can we solve this?

One way would be to put our calculation outside the component, simply getting the initial value once and passing it as a value later on. While that’s possible, this is typically impractical since if this function is always going to calculate the same value, why not just pre-calculate this value beforehand and pass it as a const to your state variable? And in most cases, we’ll probably need to wait on a prop value or some other dynamic value that is required, so that’s a no go.

To solve this, useState actually offers a lazy initialization, meaning that it is able of calling a function once when needed, and never calling this function again later on. The way to do this is by passing useState a function directly

Using useState like this, getValue will still make a choppy mount experience for this component but for further rerender, like when clicking the button, things will be smooth again since it knows to not call this function every again, in a very lazy fashion (the good type of lazy). And that’s that.

Born Identity

One last thing I want to talk about before the bell rings and letting you go off to recess — one very important but often overlooked characteristic about useState and setState specifically is that React guarantees that this function has a stable identity. That’s a fancy way of saying the React will always return the same function (that is, the same reference to the same function object) on every render, be it the first or the thousandth render. Check this out below:

Give the button a ride and click a few times and check your console. Trust me, they are the same.

⚠️ In case you see false in your console, it’s because I’ve been lying to you this whole time. Just kidding, try removing <React.StrictMode> in case you’re using it or leave a comment for me to explain this better :)

And you might ask, “Ricardo, why end your post talking about something so… unimportant?”, to which I respond “Gasp! Unimportant?”, but I understand the concern, so I’ll give you part of the answer — when we get to our next hook, you’ll see that reference stability is very important. So stick around to learn more about hooks and in special, our next hook friend, useEffect .

Catch you guys on the flippity flip and until then ✌️

--

--

Ricardo Pedroni
RPEDRONI

O Professor Ricardo Pedroni ensina conceitos importantes e boas práticas de desenvolvimento de projetos em software. YouTube https://bit.ly/3q0TIAU