Skip to content
This page has been auto-translated and may contain errors.View in English

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:

jsx
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:

jsx
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:

jsx
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:

jsx
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:

jsx
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:

jsx
// 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:

jsx
// 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á:

jsx
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:

jsx
// 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.

Un par de hábitos prácticos te ahorran problemas aquí. Usa la forma actualizadora (setCount(c => c + 1)) por defecto siempre que el valor siguiente se construya a partir del anterior, y mantén la forma simple (setCount(0)) para establecer un valor fresco que ignore el anterior. Esa única regla evita la mayoría de sorpresas de tiempo antes de que tengas que pensar en ellas.

Mantén el estado mínimo también. Solo almacena lo que no puedas deducir de algo que ya tienes. Si sostienes una lista de elementos en estado, no mantengas también su cantidad en estado; léela con items.length durante el renderizado. Cada pieza extra de estado es otro valor para mantener sincronizado, y el estado duplicado es exactamente donde dos copias se desvían. Cuando varios valores siempre cambian juntos, un objeto único o un hook useReducer frecuentemente es más estable que un montón de llamadas useState separadas.

Ayuda pensar en el estado como una instantánea. Para cualquier renderizado único, count es un valor fijo, decidido cuando ese renderizado comenzó e inmutable mientras se ejecuta. setCount no reasigna esa variable; le pide a React que renderice de nuevo, y el nuevo valor solo aparece en la instantánea del siguiente renderizado. Así que leer estado inmediatamente después de establecerlo te da el valor anterior:

jsx
function handleClick() {
  setCount(count + 1)
  console.log(count) // aún el valor de este renderizado, no el nuevo
}

Aquí es también de donde vienen los closures obsoletos. Cada función que defines durante un renderizado, manejadores de eventos, efectos, temporizadores, cierra sobre los valores de estado de ese renderizado. Si uno de ellos se ejecuta después, un callback de setTimeout por ejemplo, aún ve el count que capturó, aunque el valor haya avanzado desde entonces. La forma actualizadora elude esto, porque setCount(c => c + 1) le pide a React el valor actual en el momento en que se ejecuta en lugar de confiar en el capturado.

El batching es la otra mitad del panorama. React agrupa las actualizaciones de estado que ocurren dentro de un único manejador de eventos y renderiza una vez, después de que el manejador termine, en lugar de después de cada setCount. Por eso llamar al setter dos veces con count + 1 aún solo mueve el valor en uno: ambas llamadas se ponen en cola contra la misma instantánea antes de que ocurra ningún renderizado. Las funciones actualizadoras, por el contrario, se ponen en cola y se ejecutan en orden una contra la otra, así que se componen. Desde React 18 este batching se extiende a actualizaciones dentro de promesas, timeouts, y otros callbacks asincronos también, así que las mismas reglas aplican bien más allá de manejadores de eventos simples.

JunoEstado es la memoria de un componente Una variable simple olvida su valor cada vez que se ejecuta la función, y 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.

JunoEstado es la memoria de un componenteuseState te da un valor y un setter, y llamar al setter es lo que dispara un re-renderizado. Usa la forma actualizadora cuando el siguiente valor depende del actual o cuando actualizas varias veces seguidas.

El estado es por instancia, así que dos del mismo componente mantienen valores separados, y siempre le pasas al setter un nuevo valor en lugar de mutar el array u objeto anterior. Para una lista, eso significa map: una copia nueva del elemento que estás cambiando, todo lo demás sin tocar.

JunoEstado es la memoria de un componente Trata el estado como una instantánea: fijo para la duración de un renderizado, actualizado solo en el siguiente, por lo que leerlo inmediatamente después de establecerlo da el valor anterior y por qué los closures capturados se quedan obsoletos.

React agrupa las actualizaciones en un evento y renderiza una vez, así que las llamadas setter contra la misma instantánea no se apilan, mientras que las funciones actualizadoras sí.

Cuando la corrección depende del valor anterior, usa la forma actualizadora, y cuando el valor inicial es costoso de construir, dale a useState la función misma para que React la ejecute una vez.

Siguiente: Events, donde esos manejadores onClick que has estado escribiendo reciben un vistazo adecuado.