TL;DR
- Let's understand what State is.
- Understand the process of State updates.
- Understand the useState hook.
As we learned in the previous post, React composes UI through components.
Components sometimes need to change what's on the screen as a result of interaction. In this case, they need to remember things like the current input value, current image, etc. React calls this kind of component-specific memory State.
When using simple local variables, React doesn't recognize these values.
To solve this, React provides the following two things through the useState hook:
state variable to retain data between renderssetState function to update the variable and trigger the component to render again(Let's learn more about the useState hook below.)
State is local to a component instance. This means that if you render the same component multiple times, each instance maintains its own independent state.
So how does rendering work in React?
Taking the guest's order to the kitchen
There are two reasons why a component renders:
1. Component's initial render
When the app starts, calling createRoot followed by render triggers the initial render.
2. Component's state has been updated
Calling setState updates the component's state, causing the component to re-render. Updating a component's state automatically queues a render.
Preparing the order in the kitchen Rendering is React calling your components.
Serving the dish to the table
After rendering your components, React will modify the DOM.
appendChild() DOM API to put all the DOM nodes it has created on screen.In other words, React only changes the DOM nodes if there's a difference between renders.
State variables in React behave like snapshots. Setting a state variable doesn't change the state variable you already have, but triggers a re-render.
In React, rendering takes a snapshot of that moment. That is, props, event handlers, local variables, etc. are all based on the state at the time of rendering.
The process of React re-rendering a component is as follows:
The values are fixed when UI snapshot is taken by calling the component! Let's look at this with code for easier understanding.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}Looking at the code flow, it seems like 5 should be output since we increased the number state by 5, but 0 is output because the number in that JSX was already captured as 0 in the snapshot. (State is scheduled at the moment of user interaction)
To solve this phenomenon, we can use an updater function.
React waits for all code in event handlers to complete before processing state updates. This prevents excessive re-renders and allows multiple states to be updated at once. This is called batching.
When applying batching behavior to the same state variable, you can pass a function that calculates the next state based on the previous state in the queue. This is called an updater function.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1); // updater function
}}>+5</button>
</>
)
}📌 Recap
- State refers to the memory of React components.
- React only changes DOM nodes if there's a difference between renders.
- State in React behaves like snapshots.
- A function that calculates the next state based on the previous state is called an
updaterfunction.
useStateimport { useState } from 'react'
const [index, setIndex] = useState(0);setIndex(index + 1). This tells React to remember that index is 1 and triggers another render.useState(0), but because React remembers that you set index to 1, it returns [1, setIndex] instead.When using multiple states, structuring the state may be necessary.
If you always update two or more state variables at the same time, consider merging them into a single state variable.
// AS_IS
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// TO_BE
const [position, setPosition] = useState({ x: 0, y: 0 });Multiple states should not contradict and be inconsistent with each other.
If you can calculate some information from the component's props or its existing state variables during rendering, you should not put that information into that component's state.
Don't mirror props in state! State is only initialized during the first render, so if you store received props directly in state, the state variable won't update when different props are passed.
Mirroring props in state only makes sense when you want to ignore all updates for a specific prop. By convention, start the prop name with
initialordefault.
Avoid duplication of the same data.
If your state is deeply nested, consider flattening it.
💡 How does React know which state to return?
Hooks in React rely on a stable call order on every render of the same component. As long as you follow the rule of calling hooks at the top level, hooks are always called in the same order.
Internally, React holds an array of state pairs for every component. It also maintains the current pair index, which is set to 0 before rendering. Each time you call
useState, React gives you the next state pair and increments the index.Looking directly at React's code, you can see how it remembers the next state pair:
// ReactFiberHooks.js function updateWorkInProgressHook() { let nextCurrentHook: null | Hook; if (currentHook === null) { const current = currentlyRenderingFiber.alternate; if (current !== null) { nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } else { nextCurrentHook = currentHook.next; } }
const [count, setCount] = useState(0)
function onClickCounter(amount) {
// setCount((count) => count + adjustment)
setCount(count + adjustment) // This approach only changes by 1 no matter how many times you call it
}React schedules state updates. Therefore, theoretically, using a non-functional approach could rely on outdated or incorrect state snapshots.
The useState hook itself is a synchronous function. However, the re-rendering process is asynchronous. (batching)
Reference types are passed by reference, so even with the same value, they are different objects. Therefore, re-rendering occurs even when passing the same value.
I was able to deeply understand State, which is a core concept of React. In particular, I was able to learn how it works by looking directly at React code. May the day come when I can contribute to code beyond just documentation!
What I learned through this post
- State in React refers to the memory of components.
- State in React behaves like snapshots.
- React decides which state to return by remembering the next state pair.
- I was able to understand how it works by looking directly at React code.
References