Estado
Una variable común dentro de una función se olvida en el momento en que la función termina, así que no puede mantener un valor que cambia con el tiempo y se muestra en pantalla. Ese vacío es lo que estado llena: es cómo un componente recuerda algo entre renders. Mientras que props son datos que pasan desde afuera y que un componente nunca cambia, el estado es data que el componente posee y actualiza a sí mismo. El hook useState le da a un componente esa memoria, y te da una forma de actualizar el valor que también le indica a React que redibuque.
Acá está un contador:
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}useState(0) establece el valor inicial y devuelve un par: el valor actual, count, y una función para cambiarlo, setCount. Los nombres son tuyos; la forma [value, setValue] es la convención. El onClick del botón ejecuta una función cada vez que se hace clic, que el capítulo Events cubre completamente después, así que por ahora acéptalo. Esa función llama a setCount(count + 1), y la llamada hace dos cosas. Guarda el nuevo valor, y redibuja Counter para que la pantalla muestre el número actualizado. Llamar al setter es la señal a React de que algo cambió y el componente debe ejecutarse de nuevo.
Actualizar basándose en el valor anterior
A menudo verás el setter llamado con una función en lugar de un valor simple:
setCount(c => c + 1)Esta es la forma de actualizador. React llama a tu función con el valor más reciente y usa lo que devuelvas como el próximo estado. Úsala siempre que el nuevo valor se construya a partir del anterior, y especialmente cuando actualizas más de una vez seguidas. Estas dos líneas no hacen lo que parecen:
setCount(count + 1)
setCount(count + 1)Ambas lecturas de count ven el mismo valor de este render, así que juntas solo mueven el contador en uno. La forma de actualizador lo arregla, porque cada llamada recibe el resultado de la anterior:
setCount(c => c + 1)
setCount(c => c + 1)Ahora el contador sube en dos. Cambiar un booleano funciona de la misma manera y es donde más encontrarás la forma de actualizador: setIsOpen(open => !open) toma el valor actual y devuelve su opuesto. Cuando tu próximo valor depende del actual, la forma de actualizador es el default seguro.
El estado es por instancia de componente
Cada vez que renderizas un componente, obtiene su propio estado separado. Dos contadores en la misma página cada uno mantiene su propio contador:
function App() {
return (
<>
<Counter />
<Counter />
</>
)
}Hacer clic en el primer botón no afecta al segundo. Comparten el mismo código de Counter, y cada uno mantiene un valor independiente. El estado pertenece a cada instancia en pantalla, así que una función puede controlar muchas copias separadas.
Reemplaza el estado, no lo mutes
Actualizas el estado dándole a React un nuevo valor, y React compara ese valor con el anterior para decidir qué cambió. Cambiar el valor existente en su lugar omite esa señal, así que la pantalla se vuelve obsoleta. Esto importa especialmente con objetos y arrays. Para añadir a un array, construye uno nuevo en lugar de hacer push en el anterior:
// Omite la actualización: React nunca ve un valor nuevo
todos.push(newTodo)
setTodos(todos)
// Funciona: un array completamente nuevo
setTodos(prev => [...prev, newTodo])Lo mismo aplica para objetos: expande el anterior en un nuevo objeto con tu cambio aplicado, y devuelve eso desde el actualizador. Trata el valor que obtienes de useState como de solo lectura, y siempre dale al setter algo nuevo.
El anidamiento necesita cuidado. Expandir un objeto copia su nivel superior, así que un objeto anidado dentro de él sigue siendo el mismo objeto, y cambiar ese anidado llega hasta el original:
// Muta el original: el objeto address anidado sigue siendo compartido con el nuevo
user.address.city = 'Ciudad de México'
setUser({ ...user })
// Funciona: un nuevo objeto en cada nivel hasta el cambio
setUser(prev => ({
...prev,
address: { ...prev.address, city: 'Ciudad de México' }
}))Los paréntesis alrededor del objeto hacen que la función flecha lo devuelva en lugar de leerlo como un bloque de código. Cada nivel hasta el cambio necesita su propio spread, lo que se vuelve incómodo mientras el anidamiento se profundiza. Cuando te encuentras escribiendo una cadena de ellos, eso usualmente es una señal de que el estado quiere aplanarse en piezas más pequeñas.
Cambiar un elemento en una lista
Cambiar un elemento que ya está en un array es donde la gente más a menudo se atasca. La herramienta para esto es map. Recorre el array, devuelve una copia cambiada del elemento que buscas, y devuelve cada otro elemento como está:
function toggleTodo(id) {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
))
}El setter toma la forma de actualizador, ya que el próximo array se construye a partir del actual. El ternario hace la selección: el todo que coincide obtiene una copia con done invertido, y todo lo demás pasa directo. Esos elementos que pasan son los mismos objetos que siempre fueron, lo que está bien. Solo el que cambiaste necesita ser nuevo, junto con el array que los sostiene, y map construye ese array fresco para el setter.
Remover un elemento es filter, y añadir uno es el spread de la sección anterior. Entre los tres, la mayoría de las actualizaciones de listas están cubiertas.
Cuando el valor inicial es costoso
JavaScript evalúa el argumento que pasas a useState en cada render, antes de que useState en sí se ejecute, y React mantiene el resultado solo la primera vez. Para un valor como 0, eso no cuesta nada. Para una llamada de función que hace trabajo real, se ejecuta en cada render y el resultado se descarta después del primero:
// buildBoard() se ejecuta en cada render, y su resultado se descarta después del primero
const [board, setBoard] = useState(buildBoard())
// buildBoard se llama una vez, en el primer render
const [board, setBoard] = useState(buildBoard)La primera línea llama a la función y le da a React el resultado. La segunda le da a React la función en sí y deja que React la llame una vez. Guarda esto para trabajo que sea genuinamente costoso, como leer del almacenamiento, generar una estructura grande, o una computación pesada. En useState(0) es ruido.
Nota de versión
En componentes de clase, el estado vivía en this.state y lo actualizabas con this.setState. useState es el equivalente de componentes de función, y es la forma estándar de mantener estado en React moderno.
useState lo recuerda. Obtienes el valor y un setter, y llamar a ese setter tanto almacena el nuevo valor como redibuja el componente. Cuando el nuevo valor se construye sobre el anterior, usa setCount(c => c + 1) para que siempre estés trabajando desde el más reciente. Cuando ese valor es un array u objeto, dale al setter uno completamente nuevo con tu cambio aplicado. Siguiente: Events, donde esos manejadores onClick que has estado escribiendo obtienen una mirada adecuada.

