跪拜 Guibai
← Back to the summary

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:


A User Filtering Demo to Understand React State Snapshots and Lazy Initialization

When first learning React, two questions arise most easily:

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('')

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

  1. A state update triggers the next render; the current state variable is an old snapshot.
  2. value + onChange is the fundamental pattern for React controlled forms.
  3. Data that can be calculated from existing state should be prioritized as derived data.
  4. 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.