React useState
useState gives a function component its own piece of memory that survives re-renders and triggers a new render whenever it changes.
Declaring State
Calling useState(initialValue) returns a two-element array: the current value and a setter function to update it. Array destructuring — const [value, setValue] = useState(initial) — lets you name both however you like. The argument you pass is used only on the very first render; every render after that ignores it.
Updating State Correctly
Unlike the setState method on class components, the setter returned by useState replaces the value entirely — it does not merge objects for you. When the next value depends on the current one, pass the setter a function instead of a value; React calls it with the latest state and uses its return value, which avoids bugs caused by stale values inside closures.
- The initial value argument is only used on the component's first render
- Calling the setter schedules a re-render; it does not update the variable immediately in the same line
- State is preserved between renders as long as the component stays mounted in the same position in the tree
- Never mutate state directly (no array.push or object.field = x); always create a new value
- Use the functional updater form — setCount(prev => prev + 1) — whenever the new value depends on the old one
A counter with functional updates
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount((prev) => prev + 1);
}
function decrement() {
setCount((prev) => prev - 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
export default Counter;
State does not have to be a single primitive value — it can just as easily hold an object or an array, as long as you replace it immutably. Spreading the previous value into a new object or array keeps the fields you did not touch while updating the ones you did.
Object state in a form
import { useState } from "react";
function SignupForm() {
const [form, setForm] = useState({ name: "", email: "" });
function handleChange(event) {
const { name, value } = event.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
return (
<form>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<p>Hello, {form.name || "stranger"}!</p>
</form>
);
}
export default SignupForm;
Array state for a todo list
import { useState } from "react";
function TodoList() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState("");
function addTodo() {
if (text.trim() === "") return;
setTodos((prev) => [...prev, text]);
setText("");
}
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</div>
);
}
export default TodoList;
Exercise: React useState
Right after calling a state setter, what does the local variable holding the old state show?