Estado
Una variable normal dentro de una función se olvida en el momento en que la función termina, así que no puede retener un valor que cambia con el tiempo y se muestre en pantalla. Ese vacío es lo que llena el estado: es cómo un componente recuerda algo entre renderizados. Donde los props son datos que vienen de afuera y que un componente nunca cambia, el estado es dato que el componente posee y actualiza por sí mismo. El hook useState le da al componente esa memoria, y te da una forma de actualizar el valor que también le dice a React que redibuje.
Aquí hay 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 acéptalo de momento. Esa función llama a setCount(count + 1), y la llamada hace dos cosas. Almacena el nuevo valor, y renderiza nuevamente 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 actualizadora. React llama a tu función con el valor más reciente y usa lo que devuelves como el siguiente 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 renderizado, así que juntas solo mueven el contador hacia arriba en uno. La forma actualizadora 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. Invertir un booleano funciona igual y es donde encontrarás la forma actualizadora más frecuentemente: setIsOpen(open => !open) toma el valor actual y devuelve su opuesto. Cuando tu valor siguiente depende del actual, la forma actualizadora es el estándar 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 Counter, y cada uno sostiene un valor independiente. El estado pertenece a cada instancia en la pantalla, así que una función puede impulsar muchas copias separadas.
Reemplaza el estado, no lo mutes
Actualizas el estado dándole a React un valor nuevo, 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 más con objetos y arrays. Para agregar a un array, construye un nuevo array en lugar de hacer push al anterior:
// Omite la actualización: React nunca ve un nuevo valor
todos.push(newTodo)
setTodos(todos)
// Funciona: un array completamente nuevo
setTodos(prev => [...prev, newTodo])Lo mismo aplica para objetos: expande el anterior en un objeto nuevo con tu cambio aplicado, y devuelve ese del actualizador. Trata el valor que obtienes de useState como 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 se refleja en el original:
// Muta el original: el objeto address anidado aún se comparte con el nuevo
user.address.city = 'Buenos Aires'
setUser({ ...user })
// Funciona: un objeto nuevo en cada nivel hasta el cambio
setUser(prev => ({
...prev,
address: { ...prev.address, city: 'Buenos Aires' }
}))Los paréntesis alrededor del objeto son lo que hace que la función flecha lo devuelva en lugar de leerlo como un bloque de código. Cada nivel hasta el cambio necesita su propia expansión, lo que se vuelve incómodo conforme el anidamiento se profundiza. Cuando te encuentras escribiendo una cadena de ellos, eso suele ser una señal de que el estado quiere aplanarse en partes 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 frecuentemente se queda atascada. La herramienta para ello es map. Recorre el array, devuelve una copia modificada para el elemento que buscas, y devuelve cada otro elemento tal como está:
function toggleTodo(id) {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
))
}El setter toma la forma actualizadora, ya que el siguiente array se construye a partir del actual. El ternario hace la selección: el todo coincidente obtiene una copia con done invertido, y todo lo demás pasa directamente. Esos elementos que pasan directamente son los mismos objetos que siempre fueron, lo cual está bien. Solo el que cambiaste necesita ser nuevo, junto con el array que los contiene, y map construye ese array fresco para el setter.
Remover un elemento es filter, y agregar uno es la expansión de la sección anterior. Entre los tres, la mayoría de actualizaciones de listas están cubiertas.
Cuando el valor inicial es costoso
JavaScript evalúa el argumento que pasas a useState en cada renderizado, antes de que useState mismo 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 renderizado y el resultado se descarta después del primero:
// buildBoard() se ejecuta en cada renderizado, y su resultado se descarta después del primero
const [board, setBoard] = useState(buildBoard())
// buildBoard se llama una vez, en el primer renderizado
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 misma y deja que React la llame una vez. Guarda esto para trabajo 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 componente de función, y es la forma estándar de mantener estado en React moderno.
useState lo recuerda. Recuperas 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 a partir del anterior, usa setCount(c => c + 1) para siempre trabajar 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 reciben un vistazo adecuado.

