A Single Input Box Exposes React's State Snapshot and Lazy Init Traps
title: A User Filtering Demo to Understand React State Snapshots and Lazy Initialization description: Starting from an input box filtering a list, build a complete mental model of useState, derived data, keys, and performance optimization. tags:
- React
- Hooks
- Performance Optimization
A User Filtering Demo to Understand React State Snapshots and Lazy Initialization
When first learning React, two questions arise most easily:
- Why does printing the state immediately after
setStatestill show the old value? - Why does
useState(heavyCalculation())affect input filtering performance?
A user filtering list can tie these two problems together.
This article is organized based on the learning code from state-demo and has not been run to verify.
The Data Flow Behind the Page
The page has two types of data:
const [users] = useState(() => heavyCalculation())
const [filterText, setFilterText] = useState('')
users: The original user data, which only needs to be created once.filterText: The filter condition the user is typing, which changes frequently.
The filtered data is not stored as state but is calculated on every render:
const filteredUsers = users.filter(user =>
user.name.includes(filterText)
)
This illustrates an important principle: For data that can be derived, don't create a duplicate copy in state.
Controlled Input: React Manages the Input Value
<input
value={filterText}
onChange={event => setFilterText(event.target.value)}
/>
It does not let the DOM save the input content by itself; instead, it is controlled by state:
User types
→ onChange
→ setFilterText
→ App function re-executes
→ value uses the new filterText
This pattern is called a controlled component. The advantage is that the input value is always in React state and can be directly used for filtering, validation, submission, and clearing.
React State Is a Snapshot, Not an Immediate Variable Modification
Suppose a counter is written like this:
setCount(count + 1)
console.log(count)
The console gets the old count. This is not a React failure; the current function is still using "the state snapshot of this render."
Current snapshot: count = 0
setCount(1): Requests the next render to update to 1
console.log(count): Still reads 0 from the current snapshot
Next render: count = 1
When updating consecutively based on an old value, using a functional update is more stable:
setCount(previousCount => previousCount + 1)
List Rendering: map + key
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
map is responsible for converting data into JSX; key tells React "who this list item is."
List items change before and after filtering. A stable user.id helps React correctly determine node additions, removals, and reuse. Do not default to using array indices, especially when lists can add, remove, or sort items.
Performance Trap: How Many Times Does the Initialization Function Execute
The heavyCalculation in the demo loops to generate 10,000 user records. The two patterns below differ only by an arrow function:
// Every time the component function executes, JS runs heavyCalculation first
const [users] = useState(heavyCalculation())
// React only executes heavyCalculation on the first mount
const [users] = useState(() => heavyCalculation())
Why?
useState(heavyCalculation())
Can be understood as:
const result = heavyCalculation()
useState(result)
When you type a filter term, the filterText update causes the component function to re-execute, so heavyCalculation() runs again. Although React will not use this new initial value to overwrite the existing users, the computation cost has already been paid.
Whereas:
useState(() => heavyCalculation())
Passes in a function. React treats it as a lazy initializer, calling it only on the first mount.
How to Prove Code Time Consumption
const start = performance.now()
const result = heavyCalculation()
console.log(performance.now() - start)
performance.now() is suitable for measuring JavaScript execution time. It turns "it feels like it might be slow" into comparable millisecond data.
Finally, Remember These 4 Sentences
- A state update triggers the next render; the current state variable is an old snapshot.
value + onChangeis the fundamental pattern for React controlled forms.- Data that can be calculated from existing state should be prioritized as derived data.
- Use
useState(() => heavyCalculation())for expensive initial values to avoid wasted recalculations.
These concepts are basic, but they appear repeatedly in React interviews and real business pages.