Ever felt stuck when dealing with state updates in your React apps? Trust me, you're not alone! Lots of developers (including myself) have scratched their heads trying to figure out why their apps sometimes act weird when handling updates.
Imagine, you've just put your shiny new app online, but then users start complaining about things loading slowly or buttons not working right away. Not fun at all, right?
But here's the good news - there's a simple way to fix this using useState
! In this post, I'll show you some easy tricks to make your React app work smoothly and keep your users happy.
Understanding the useState hook
Let start by a simple example of useState usage:
State is use for values that change over time. Whenever we have “dynamic” values, we need to use React state.
To create a state variable, we use the useState
function. This function takes a single argument: the initial value. In this case, that value initializes to 0
. This value is chosen because when the page first loads, we've clicked the button 0 times.
useState is a hook.
A hook is just a special React function that helps us tap into React's core features and make our components more powerful.
The useState
hook returns an array containing two items:
- The current value of the state variable. We've decided to call it
count
. - A function we can use to update the state variable. We named it
setCount
.
Naming conventions
When we create a state variable, we can name the two variables whatever we want. For example, this is equally valid:
const [hello, world] = React.useState(0);
That said, it's customary to follow the “x, setX” convention:
const [user, setUser] = React.useState();
const [errorMessage, setErrorMessage] = React.useState();
const [studentEmail, setStudentEmail] = React.useState();
The first destructured variable is the name of the thing we're tracking. The second variable prefixes that name with set
, signifying that it's a function that can be called to change the thing. This is sometimes referred to as a “setter function”, since it sets the new value of the state variable.
Initial value
React state variables can be given an initial value:
const [count, setCount] = React.useState(1);
console.log(count); // 1
We can also supply a function. React will call this function on the very first render to calculate the initial value:
const [count, setCount] = React.useState(() => {
return 1 + 1;
});
console.log(count); // 2
This is sometimes called an initializer function. It can occasionally be useful if we need to do an expensive operation to calculate the initial value. For example, reading from Local Storage:
const [count, setCount] = React.useState(() => {
return window.localStorage.getItem('saved-count');
});
If you're not familiar with the Local Storage API, it's a way for us to save values on the user's device, so that it persists even after the browser tab is closed, and can be accessed on their next visit.
The benefit here is that we're only doing the expensive work (reading from Local Storage) once, on the initial render, rather than doing it on every single render.
Let suppose we have two forms of code inside the useState:
// Form 1:
const [count, setCount] = React.useState(
window.localStorage.getItem('saved-count')
);
// Form 2:
const [count, setCount] = React.useState(() => {
return window.localStorage.getItem('saved-count');
});
In the form 1, whenever we render the Counter component, this function will be called, and all the code inside will run. We’ll call window.localStorage on every single render, and pass the value into React.useState.
In the secondary form, we are creating a function:
() => {
return window.localStorage.getItem('saved-count');
}
This function is being passed into React.useState(). And so it’s up to React to decide what to do with it.
On the very first render, React will call this function to calculate the initial value. On subsequent renders, however, React ignores the function. The initial value has already been calculated, and so there's no reason to call the function again.
Core React loop
When we use the setter function (like setCount
) to change a value in our app, React automatically updates what we see on the screen. Pretty cool, right? But ever wondered how this magic happens?
Each render is like taking a snapshot! We generate a description that shows what the UI should look like, based on the component's props/state. It's like a photo that captures what things were like at a moment in time.
This process is known as reconciliation. Through optimized algorithms, React determines what has changed—for example, detecting when a button's text updates from "Value: 0" to "Value: 1".
After identifying these differences, React proceeds to commit the changes. It updates the DOM precisely, modifying only the necessary elements.
This is the fundamental flow of React, the core loop. We can visualize this sequence like this:
- Mount → When we render the component for the first time, there is no previous snapshot to compare to. And so, React will create all of the necessary DOM nodes from scratch, and inject them into the page.
- Trigger → Eventually, something happens that triggers a state change, invoking the “set X” function (eg. setCount). We're telling React that the value of a state variable has just been updated.
- Render → Because the state changed, we need to generate a new description of the UI! React will invoke our component function again, producing a new set of React elements.
With this new snapshot in hand, React will reconcile it, comparing it to the previous snapshot, and figuring out what needs to happen in order for the DOM to match this latest snapshot. - Commit → If any DOM updates are required, React will perform those mutations (eg. changing the text content of a DOM node, creating new nodes, deleting removed nodes, etc).
Once the changes have been committed, React goes idle, and waits for the next trigger, the next state change.
Rendering vs painting
Let's look at an example. Suppose I have the following component:
function AgeLimit({ age }) {
if (age < 20) {
return (
<p>You're not old enough!</p>
);
}
return (
<p>Hello, adult!</p>
);
}
Our AgeLimit
component checks an age
prop and returns one of two paragraphs.
Now, let's suppose we re-render this component, and wind up with the following before/after pair of snapshots:
age: 16
{
type: 'p',
key: null,
ref: null,
props: {},
children: "You're not old enough!",
}
age: 17
{
type: 'p',
key: null,
ref: null,
props: {},
children: "You're not old enough!",
}
In both cases, age
is less than 20, and so we wind up with the exact same UI. As a result, no DOM mutation happens at all.
So, when we talk about “re-rendering” a component, we aren't necessarily saying that anything will change in the DOM! We're saying that React is going to check if anything's changed.
If React spots a difference between snapshots, it'll need to update the DOM, but it will be a precisely targeted minimal change.When React does change a part of the DOM, the browser will need to re-paint.
A re-paint is when the pixels on the screen are re-drawn because a part of the DOM was mutated. This is done natively by the browser when the DOM is edited with JavaScript (whether by React, Angular, jQuery, vanilla JS, anything).
To summarize:
- A re-render is a React process where it figures out what needs to change (AKA. “reconciliation”, the spot-the-differences game).
- If something has changed between the two snapshots, React will “commit” those changes by editing the DOM, so that it matches the latest snapshot.
- Whenever a DOM node is edited, the browser will re-paint, re-drawing the relevant pixels so that the user sees the correct UI.
- Not all re-renders require re-paints! If nothing has changed between snapshots, React won't edit any DOM nodes, and nothing will be re-painted.
The critical thing to understand is that when we talk about “re-rendering”, we're not saying that we should throw away the current UI and re-build everything from scratch.
React tries to keep the re-painting to a minimum, because re-painting is slow. Instead of generating a bunch of new DOM nodes from scratch (lots of painting), it figures out what's changed between snapshots, and makes the required tweaks with surgical precision.
Asynchronous updates
Consider the following code. What value would you expect to see in the developer console when the user clicks the button for the first time❓
function App() {
const [count, setCount] = React.useState(0);
return (
<>
<p>
You've clicked {count} times.
</p>
<button
onClick={() => {
setCount(count + 1);
console.log(count)
}}
>
Click me!
</button>
</>
);
}
Reveal the answer
The answer is 0
Pretty mysterious, right?
When we create our state variable, we initialize it to 0
. Then, when we click the button, we increment it by 1, to 1
. So shouldn't it log 1
, and not 0
?
Here's the catch: state setters aren't immediate.
When we call setCount
, we tell React that we'd like to request a change to a state variable. React doesn't immediately drop everything; it waits until the current operation is completed (processing the click), and then it updates the value and triggers a re-render.
For now, the important thing to know is that updating a state variable is asynchronous. It affects what the state will be for the next render. It's a scheduled update.
Here's how to fix the code so that we have access to the newer value right away:
function App() {
const [count, setCount] = React.useState(0);
return (
<>
<p>
You've clicked {count} times.
</p>
<button
onClick={() => {
const nextCount = count + 1;
setCount(nextCount);
console.log(nextCount)
}}
>
Click me!
</button>
</>
);
}
Rather than passing the expression directly to the state-setter function setCount(count + 1)
, we store it in a variable first. This allows us to use that variable in our console.log
statements or anywhere else within the onClick
handler.
I prefer using the prefix "next" (like nextCount
or nextUser
) because it clearly indicates we're working with the future state value—what it will be on the next render. Of course, this naming convention is just my preference; feel free to name these variables in a way that makes sense to you.
🔍. Similar posts
How to Effectively Use Event Handlers in React with Arguments
28 Jan 2025
The Simple Guide to React Range Utility for Effective Coding
26 Jan 2025
Everything You Need to Know About React Conditional Logic
22 Jan 2025