How does useState work? If I call the set function, does the value change immediately?
Show answer — try answering out loud first
useState asks React to store a value across renders and gives you a function to update it. The set function does NOT change the value immediately: it schedules a new render, and it's in that next render where the variable already holds the new value.
A function component runs in full on every render. Normal local variables are lost between executions, so React needs an external place to store the state. That's what useState does.
The key points:
- State is a snapshot per render. Within a render, the state variable is a constant. Even if you call
setContador(5), thecontadorvariable in that render still holds what it held before. - The set schedules a render, it doesn't mutate. React groups (batches) several updates and re-runs the component with the new value.
- Functional form: if the new value depends on the previous one, use
setContador(prev => prev + 1). That way React passes you the most recent value and you avoid working with a stale snapshot. - Objects and arrays are immutable in practice. Don't modify the existing object: create a new one with spread (
{ ...persona, edad: 30 }). If you mutate, React may not detect the change because it compares references. - Lazy initialization: if the initial value is expensive to compute, pass a function
useState(() => calcularCaro()). React only runs it on the first render.
import { useState } from "react";
function Contador() {
// Lazy initialization: the function only runs on the first render
const [contador, setContador] = useState<number>(() => {
const guardado = localStorage.getItem("contador");
return guardado ? Number(guardado) : 0;
});
const [tags, setTags] = useState<string[]>([]);
function sumarTres() {
// Functional form: each call receives the most recent value
setContador(prev => prev + 1);
setContador(prev => prev + 1);
setContador(prev => prev + 1);
console.log(contador); // prints the value BEFORE adding: the set is not immediate
}
function agregarTag(tag: string) {
// Immutability: we create a new array, we don't do tags.push(tag)
setTags(prev => [...prev, tag]);
}
return (
<div>
<p>Valor: {contador}</p>
<button onClick={sumarTres}>+3</button>
<button onClick={() => agregarTag("react")}>Agregar tag</button>
<ul>{tags.map(t => <li key={t}>{t}</li>)}</ul>
</div>
);
}
Calling set several times using the render's variable instead of the functional form:
// WRONG: all three calls use the same snapshot (contador = 0)
setContador(contador + 1);
setContador(contador + 1);
setContador(contador + 1);
// Result: the counter ends at 1, not 3
// RIGHT: the functional form chains the updates
setContador(prev => prev + 1);
setContador(prev => prev + 1);
setContador(prev => prev + 1);
// Result: the counter ends at 3
The same mistake shows up with objects: mutating with persona.edad = 30 and then setPersona(persona) doesn't re-render, because the reference is the same. The fix is setPersona({ ...persona, edad: 30 }).
useState stores a value that survives across renders and gives me a function to update it. The important thing is that the set is not immediate: it schedules a new render and the new value appears only in that next execution, because within a render the state is a fixed snapshot. When the new value depends on the previous one, I use the functional form
setX(prev => ...)so I don't work with stale data. With objects and arrays I never mutate: I create new copies with spread so React detects the change. And if the initial value is expensive to compute, I pass a function so it only runs on the first render.
Create a ListaCompras component with a productos: string[] state and a controlled input. On pressing "Agregar", the product is added to the list without mutating the array, and the input is cleared.
See solution
import { useState } from "react";
function ListaCompras() {
const [productos, setProductos] = useState<string[]>([]);
const [texto, setTexto] = useState("");
function agregar() {
if (texto.trim() === "") return;
// New copy of the array: no productos.push(texto)
setProductos(prev => [...prev, texto.trim()]);
setTexto(""); // we clear the input
}
return (
<div>
<input value={texto} onChange={e => setTexto(e.target.value)} />
<button onClick={agregar}>Agregar</button>
<ul>{productos.map((p, i) => <li key={`${p}-${i}`}>{p}</li>)}</ul>
</div>
);
}