In one minute
useState gives a component a value React remembers between renders, plus a function to change it: const [query, setQuery] = useState(''). Calling setQuery doesn’t change query in the running code; it asks React to render again, and in that next render useState returns the new value. Three habits make state code reliable: make inputs controlled (state is written back with value), derive anything you can compute instead of storing it twice, and pass a function to useState when the first value is expensive to compute.
You’ll be able to: wire an input to state correctly, avoid duplicated state, and know when the lazy initializer matters.
1function Search() {2 const [query, setQuery] = useState('')3 return (<>4 <input onChange={e => setQuery(e.target.value)} />5 <input type="checkbox" name="office"6 onChange={e => toggle('office', e.target.checked)} />7 <Results query={query} />8 </>)9}- event
onChange={e => toggle('office', e.target.checked)} />The user typed “lamp”, then ticks office. The checkbox adds the word to the query. - render phase
const [query, setQuery] = useState('')State changes to"lamp office"and React re-renders. - render phase
<input onChange={e => setQuery(e.target.value)} />Nothing tells the input what to show, so it keeps its own text. Recorded: the box still read"lamp"while the results used"lamp office". <input type="checkbox" name="office"It fails the other way too: typing “audio” didn’t tick the audio checkbox (recorded:audioChecked: false). Two copies of the truth, already out of sync.
1function Search() {2 const [query, setQuery] = useState('')3 const words = query.split(' ')4 return (<>5 <input value={query}6 onChange={e => setQuery(e.target.value)} />7 <input type="checkbox" name="office"8 checked={words.includes('office')}9 onChange={e => toggle('office', e.target.checked)} />10 <Results query={query} />11 </>)12}- event
onChange={e => toggle('office', e.target.checked)} />The same click on office. - render phase
const [query, setQuery] = useState('')The same state change. - render phase
<input value={query}Now every render writes the state back: the input showsquery, and each checkbox is computed from its words. - commit phaseRecorded: the box read
"lamp office", and typing “audio” ticked the audio checkbox. One source of truth.
The example: product search with category filters
The product store’s search box narrows products by name or category. Ticking a category checkbox adds its name to the query ("lamp office").
First attempt: onChange only
function SearchUncontrolled() {
const [query, setQuery] = useState('')
function toggle(category: string, checked: boolean) {
const words = query.split(' ').filter((w) => w && w !== category)
setQuery([...words, ...(checked ? [category] : [])].join(' '))
}
return (
<>
<input id="search" type="search" onChange={(e) => setQuery(e.currentTarget.value)} />
{categories.map((c) => (
<label key={c}>
<input type="checkbox" name={c} onChange={(e) => toggle(c, e.currentTarget.checked)} /> {c}
</label>
))}
<Results query={query} />
</>
)
}
The recorder typed “lamp”, ticked office, then replaced the text with “audio”:
{
"input": "lamp",
"officeChecked": true,
"results": "1 products: Desk Lamp"
}
{
"audioChecked": false,
"results": "2 products: Wireless Headphones, Bluetooth Speaker"
}
State and screen disagree: the results use "lamp office", but the box still shows "lamp"; and typing “audio” doesn’t tick the audio checkbox. The DOM elements keep their own values, and React only hears about changes.
Controlled and derived
function SearchControlled() {
const [query, setQuery] = useState('')
const words = query.split(' ') // derived on every render
function toggle(category: string, checked: boolean) {
const rest = words.filter((w) => w && w !== category)
setQuery([...rest, ...(checked ? [category] : [])].join(' '))
}
return (
<>
<input id="search" type="search" value={query} onChange={(e) => setQuery(e.currentTarget.value)} />
{categories.map((c) => (
<label key={c}>
<input
type="checkbox"
name={c}
checked={words.includes(c)}
onChange={(e) => toggle(c, e.currentTarget.checked)}
/>{' '}
{c}
</label>
))}
<Results query={query} />
</>
)
}
{
"input": "lamp office",
"officeChecked": true,
"results": "1 products: Desk Lamp"
}
{
"audioChecked": true,
"results": "2 products: Wireless Headphones, Bluetooth Speaker"
}
Two changes fixed both problems:
value={query}makes the input controlled: every render writes the state back into it. The browser shows what React says.checked={words.includes(c)}derives the checkboxes fromqueryinstead of giving each one its ownuseState. There’s one source of truth; the checkboxes can’t drift from it because they’re recomputed on every render.
Reading the first value from the URL
A shared link like ?query=mug should pre-fill the search:
function getQueryParam() {
log('getQueryParam() runs')
return new URLSearchParams(window.location.search).get('query') ?? ''
}
function SearchEager() {
const [query, setQuery] = useState(getQueryParam()) // called on every render
return <input id="search" value={query} onChange={(e) => setQuery(e.currentTarget.value)} />
}
function SearchLazy() {
const [query, setQuery] = useState(getQueryParam) // React calls it once
return <input id="search" value={query} onChange={(e) => setQuery(e.currentTarget.value)} />
}
The recorder opened ?query=mug and typed “ xl” (three keystrokes, three re-renders). How often getQueryParam ran:
useState(getQueryParam()):
getQueryParam() runs
getQueryParam() runs
getQueryParam() runs
getQueryParam() runs
useState(getQueryParam):
getQueryParam() runs
useState(getQueryParam()) calls the function on every render and throws the result away after the first. Passing the function itself lets React call it once, on mount. That matters for expensive work (parsing localStorage, building a big object); for useState('') or useState(0) it doesn’t.
How it works
- React stores state per component instance, in call order. Each
useStatecall takes the next slot in a list React keeps for this component. That’s why hooks can’t be called conditionally or in loops: skip one call and every later hook reads the wrong slot. (Hooks Under the Hood shows the real data structure.) setXschedules, it doesn’t assign. Inside the same handler,querystill has the old value. The new value arrives in the next render.- Controlled needs both halves.
valuewithoutonChangefreezes the field (Inputs);onChangewithoutvalueleaves the DOM in charge. - Initializers must be pure. In development Strict Mode, React calls them twice to catch side effects.
Common mistakes
- Copying a prop or another state value into its own
useState“to keep it handy”. Compute it during render instead. useState(expensiveCall())whenuseState(expensiveCall)oruseState(() => expensiveCall(arg))was meant.- Reading state right after setting it and expecting the new value.
Interview Q&A
What does useState give you?
The current value and a setter. React keeps the value outside the function, so it survives the function being called again on every render; calling the setter schedules a render in which useState returns the new value.
What makes an input "controlled"?
Passing value (or checked) from state together with an onChange that updates that state. Every render writes the state into the DOM, so React is the single source of truth. Recorded: with onChange only, ticking “office” changed the query to "lamp office" while the box still showed "lamp"; with value={query} the box showed "lamp office".
Why derive values instead of storing them in more state?
A second piece of state is a second copy that must be kept in sync by hand. If a value can be computed from existing state or props (words.includes('audio')), compute it during render: it can never be out of date. Recorded: the derived audio checkbox ticked itself when “audio” was typed.
What's the difference between useState(fn()) and useState(fn)?
useState(fn()) calls fn on every render and uses the result only the first time. useState(fn) hands React the function, which it calls once, on mount. Recorded: four calls vs one over the same three keystrokes.
Why can't hooks be called conditionally?
React matches each hook call to its stored state by order. If a hook is skipped on one render, every hook after it gets another hook’s state.
Related
- Side Effects: keeping the query in sync with the back button.
- Inputs (Fundamentals):
valuevsdefaultValue. - Hooks Under the Hood (React Internals): the hook list and update queue.
Sources
- react.dev:
useState, Choosing the state structure, Avoiding recreating the initial state - Shawn Wang, Getting Closure on Hooks
- Topic order inspired by Kent C. Dodds’ EpicReact React Hooks workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/reference/react/useState