How to useState in React
A detailed look at how React.useState works
Introduction
Since the additions of React Hooks in React 16.8, functional components allow for side effects and state management. Within state management in React, there are two hooks that can be utilized: useState
and useReducer
. This article will guide you on using useState
for state management.
Mental Model
React ships with useState
hook (function) and can be accessed through React.useState
. The method takes a single argument, the initial value for that piece of state. It then returns an array, where the 0th index is the actual state and the 1st index is used to update that state.
The conventional and more precise way to write the above code is to use ES6 array destructuring, enabling us a one-liner.
Now that we’ve seen a simple example of how to utilize the useState
API, we now know that useState
allows us to trigger a component re-render and preserve values between those renders.
Component Re-render
Under the hood, when a new argument is passed into the current state value, React causes a re-render of the component, therefore updating the UI. In the above example, upon calling our setter setFruit
and assuming that we’re passing in a new fruit
value, React will re-render and show, for example, avocado or peach, depending on which fruit we’re currently seeing.
Preserving Values between Re-renders
When invoking a function in Javascript, any values defined in that function gets garbage collected once the function finishes executing. Each subsequent call to that function produces its own unique values unless you’re utilizing closures.
Since the beginning of React Hooks, react components are now functions. It may be natural for you to think the same. However, this is not the case. The entire philosophy of React is to ensure that components can illustrate their UI based on the current state. As a result, React has to have a way to persist values between function calls, preventing them from being garbage collected even after function invocation. The public API for this, as we have already seen, is useState
.
To simply put, useState
is an instrument to preserve values between function invocations/renders and to trigger component re-rendering.
useState
When using useState
, it’s imperative to understand that each piece of the state comes with its own way to access the persisted value and updater function, following React’s composition pattern. If you’re coming from class components, you’ll find that the state management is nicely modularized as opposed to an object with multiple properties from setState
.
It is also important to note that compared to setState
in class components, useState
does not merge the new object with the previous state. If you’re brand new to React, I would not worry about this. The important characteristic to understand is that if you have a previous piece of state and you’d like to have it merged to the new state, you’ll need to include a copy of it beforehand.
A more comprehensive example of setting the current state based on the previous state is illustrated below. In summary, you’ll pass a handler function to an event attribute, such as an onClick
, which will call the updater function setFruit
.
It may also be pertinent to utilize the useReducer
hook if it so happens that the most logical data type is an object. That’s for another article, however!
Lazy State Initialization
There may be a scenario where you will need to initialize the state through a function call. To illustrate, it will look something like this.
If you were to play around with this code, you’ll notice that React uses the calculated value from getAvocadoCount
on the initial render, as well as any subsequent re-renders. In other words, getAvocadoCount
is invoked anytime the component re-renders — a computationally expensive calculation that can potentially result in a performance hit. This is not ideal at all as we would like for the initialization to be done once during the initial render only, not on subsequent renders. Fortunately, react gives us a way to navigate through this situation.
The solution is to pass useState
a function that, when invoked, will resolve to the initial state. By doing so, useState
sees that it received a function as the initial state argument, and it’ll only invoke it once on the initial render.
In summary, the solution lies in what we pass into useState
. The first example illustrates when useState
is passed a function invocation, whereas in the second example useState
is initialized a function definition. By utilizing the function definition version, state will initialize in the first render and is disregarded in subsequent renders, resulting in performance optimization.
In Summary…
useState
allows you to trigger a component re-render, add state to a function component, and preserve values between renders- enabling lazy state initialization is the result of passing in a function definition versus a function invocation