useState
Adds a local state variable to a function component and re-renders it whenever that value changes.
const [state, setState] = useState(initialState)- Added in React 16.8.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$initialState | S | (() => S) | The value the state should start as. It can be any type, but a function is treated specially: it is called once as an initializer function during the initial mount and its return value becomes the initial state. This argument is ignored after the first render. |
Return value
[state: S, setState: (nextState: S | ((prev: S) => S)) => void]An array with exactly two values: the current state (equal to initialState on the first render), and the set function that updates the state and triggers a re-render. The set function itself returns nothing.
Errors
Calling it outside the top level of a component or Hook throws "Invalid hook call"; calling the set function unconditionally during render throws "Too many re-renders".
Changelog
- PHP 18.0.0Updates are batched automatically everywhere, including inside promises, timeouts and native event handlers — not only inside React event handlers.
- PHP 18.0.0Strict Mode calls the initializer and updater functions twice in development to surface impurities; one result is discarded.
useState is the default choice for any value a component owns and re-renders on: a form field, an open/closed flag, a fetched result, a counter. Each call returns the current value and a setter; calling the setter schedules a re-render with the new value.
When to reach for it
- The value is owned by one component and its children read it through props.
- The next value is simple to compute at the call site.
- You have fewer than roughly four related pieces of state — past that,
useReducerkeeps the transitions honest.
What trips people up
The setter does not mutate the variable you already have in scope. After setCount(count + 1), count in the same function still holds the old number until the next render. When the next value depends on the previous one — especially with two updates in one handler — pass an updater function instead: setCount(c => c + 1). React queues updaters and applies them in order.
Passing a function as initialState makes it an initializer, not a state value. To actually store a function, wrap it: useState(() => myFn). And because React compares with Object.is, mutating an object or array in place and setting the same reference back skips the re-render entirely — always set a new object.
Example
When to use it
- Track the value of a controlled text input or select in a form.
- Toggle a modal, accordion or dropdown open and closed.
- Hold a list of items that buttons in the same component add to or remove from.
- Store a step index while walking a user through a multi-step wizard.
Frequently asked questions
Why is my state one step behind after calling the setter?
Because the setter only schedules the value for the next render — the variable in the current function keeps its old value until React re-renders. If you need the new value inside the same handler, compute it into a local constant and use that, and use the updater form setX(prev => ...) whenever the next value depends on the previous one.
How do I update an object or array in state?
Create a new object or array rather than mutating the existing one, e.g. setUser(prev => ({ ...prev, name })) or setItems(prev => [...prev, item]). React compares the old and new state with Object.is, so pushing onto the same array leaves the reference unchanged and the component never re-renders.
How do I store a function in state?
Wrap it in another function: useState(() => myCallback). A bare function argument is interpreted as a lazy initializer and React calls it to produce the initial state, so useState(myCallback) stores whatever myCallback returns instead of the function itself. The same applies to the setter: setFn(() => nextFn).
More examples
Lazy initial state
buildLookup runs exactly once. Typing in the input re-renders the component but does not rebuild the map, because the initializer argument is ignored after the first render. Typing "gamma" shows "length: 5"; typing "delta" shows "length: unknown word".
import { useState } from "react";
function buildLookup(words) {
// Pretend this is expensive. Without the arrow wrapper below it would
// run on every single render, not just the first one.
const map = {};
for (const word of words) {
map[word] = word.length;
}
return map;
}
const WORDS = ["alpha", "beta", "gamma"];
export default function Lengths() {
// Passing the function itself makes React call it once, on mount.
const [lookup] = useState(() => buildLookup(WORDS));
const [query, setQuery] = useState("beta");
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<p>
length: {lookup[query] ?? "unknown word"}
</p>
</div>
);
}Updating objects and arrays without mutating
The list starts at "2 remaining". Each checkbox click produces a fresh array and a fresh object for the toggled row, so Object.is sees a change and the count updates. Replacing the map call with an in-place assignment would leave the screen frozen at 2.
import { useState } from "react";
const INITIAL = [
{ id: 1, label: "Write migration", done: true },
{ id: 2, label: "Review PR", done: false },
{ id: 3, label: "Ship release", done: false },
];
export default function TodoList() {
const [todos, setTodos] = useState(INITIAL);
function toggle(id) {
// New array, new object for the changed row. Mutating
// `todo.done = !todo.done` would keep the same reference
// and React would skip the re-render.
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}
const remaining = todos.filter((t) => !t.done).length;
return (
<div>
<p>{remaining} remaining</p>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggle(todo.id)}
/>{" "}
{todo.done ? <s>{todo.label}</s> : todo.label}
</label>
</li>
))}
</ul>
</div>
);
}Resetting child state with a key
Type a draft, press "Next thread", and the textarea is empty again: the key change gave CommentBox a new identity, so its useState re-initialised. Remove the key and the half-written draft follows you to the next author, which is the usual source of "my form keeps the previous record's values" bugs.
import { useState } from "react";
function CommentBox({ author }) {
const [draft, setDraft] = useState("");
return (
<div>
<p>Replying to {author}</p>
<textarea
value={draft}
onChange={(e) => setDraft(e.target.value)}
placeholder="Write a reply"
/>
<p>{draft.length} characters</p>
</div>
);
}
const AUTHORS = ["Ada", "Grace", "Alan"];
export default function Inbox() {
const [index, setIndex] = useState(0);
const author = AUTHORS[index];
return (
<div>
<button onClick={() => setIndex((i) => (i + 1) % AUTHORS.length)}>
Next thread
</button>
{/* Changing the key unmounts the old CommentBox and mounts a fresh
one, so `draft` starts empty again for each author. */}
<CommentBox key={author} author={author} />
</div>
);
}
Discussion