Hook useState
Gestiona el estado local del componente con el hook más fundamental de React.
Receta
Tarjeta de receta de referencia rápida — lista para copiar y pegar.
const [value, setValue] = useState<T>(initialValue)
// Con inicializador perezoso (cálculo costoso)
const [value, setValue] = useState(() => computeExpensive())
// Función actualizadora (cuando el nuevo estado depende del anterior)
setValue(prev => prev + 1)Cuándo usarlo: Necesitas estado local y síncrono en un único componente.
Ejemplo en Funcionamiento
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div className="flex items-center gap-4">
<button
onClick={() => setCount(prev => prev - 1)}
className="px-3 py-1 border rounded"
>
-
</button>
<span className="text-xl font-mono w-12 text-center">{count}</span>
<button
onClick={() => setCount(prev => prev + 1)}
className="px-3 py-1 border rounded"
>
+
</button>
</div>
);
}Lo que esto demuestra:
useStatebásico con un número- Usar la función actualizadora
prev => prev + 1en lugar desetCount(count + 1)para evitar problemas de cierre obsoleto - El componente se re-renderiza solo cuando
countcambia
Análisis Profundo
Cómo Funciona
useStatedevuelve una tupla: el valor de estado actual y una función setter- En el renderizado inicial, el estado se establece en
initialValue - En renderizados posteriores, React devuelve el estado más reciente
- Cuando llamas al setter, React programa un re-renderizado con el nuevo valor
- React agrupa múltiples llamadas
setStatedentro del mismo manejador de eventos en un único re-renderizado para el rendimiento
Parámetros y Valores de Retorno
| Parámetro | Tipo | Descripción |
|---|---|---|
initialValue | T o () => T | Valor de estado inicial, o una función que lo devuelve (inicializador perezoso) |
| Retorno | Tipo | Descripción |
|---|---|---|
value | T | Valor de estado actual |
setValue | (value: T) => void o (prev: T) => T | Actualizador de estado — acepta un nuevo valor o una función actualizadora |
Variaciones
Estado de objeto:
const [form, setForm] = useState({ name: "", email: "" });
// Debe expandir para crear nueva referencia
setForm(prev => ({ ...prev, name: "Alice" }));Estado de array:
const [items, setItems] = useState<string[]>([]);
setItems(prev => [...prev, "new item"]);Inicializador perezoso (se ejecuta solo al montar):
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem("key") ?? "null");
});Notas de TypeScript
// El tipo se infiere del valor inicial
const [count, setCount] = useState(0); // number
// Genérico explícito para tipos unión o null
const [user, setUser] = useState<User | null>(null);
// Genérico explícito para tipos complejos
const [items, setItems] = useState<Item[]>([]);Trampas
Cosas que te afectarán. Cada trampa incluye qué sale mal, por qué sucede y la solución.
-
Trampa de cierre obsoleto — Leer
countdentro desetTimeoutouseEffectsin agregarlo al array de dependencias te da el valor antiguo. Solución: Usa la función actualizadorasetCount(prev => prev + 1). -
Identidad del objeto —
setState({ ...obj })crea una nueva referencia cada vez, incluso si los valores no han cambiado, causando re-renderizados innecesarios. Solución: Solo expande cuando los valores realmente cambien, o usauseMemopara valores derivados. -
Trampa del inicializador perezoso — Pasar
computeExpensive()en lugar de() => computeExpensive()ejecuta la función en cada renderizado, no solo en el primero. Solución: Siempre envuelve cálculos costosos en una función de flecha. -
Trampa de agrupamiento — Llamar a
setCount(count + 1)tres veces seguidas resulta en solo +1, no +3, porque cada llamada lee el mismocount. Solución: Usa la función actualizadorasetCount(prev => prev + 1).
Alternativas
Otras formas de resolver el mismo problema — y cuándo cada una es la mejor opción.
| Alternativa | Úsalo cuando | No lo uses cuando |
|---|---|---|
useReducer | Las transiciones de estado son complejas o dependen del estado anterior | Toggle simple o valor único |
| Zustand store | El estado se comparte entre muchos componentes no relacionados | El estado es local a un componente |
| Parámetros de búsqueda de URL | El estado debe sobrevivir a la actualización de la página y ser compartible | Actualizaciones de alta frecuencia (escribir, arrastrar) |
useRef | Necesitas un valor mutable que no dispare re-renderizados | Necesitas que la UI refleje el valor |
¿Por qué no usar siempre Zustand? Zustand añade una dependencia e indirección. useState es de costo cero para el estado local — sin provider, sin store, sin selectores. Usa la herramienta más simple que funcione.
Ejemplo del Mundo Real
De una aplicación SaaS de producción Next.js 15 / React 19 (SystemsArchitect.io).
// Ejemplo de producción: formulario de edición de FAQ con múltiples useState
// Archivo: src/components/admin/faq-edit-form.tsx
'use client';
import { useState } from 'react';
interface FaqEditFormProps {
faq: Faq & { category?: { id: string; slug: string; title: string } };
onSave: (updatedFaq: Partial<Faq>) => Promise<void>;
onCancel: () => void;
}
export default function FaqEditForm({ faq, onSave, onCancel }: FaqEditFormProps) {
const [question, setQuestion] = useState(faq.question);
const [answer, setAnswer] = useState(faq.answer);
const [isActive, setIsActive] = useState(faq.isActive);
const [sortOrder, setSortOrder] = useState(faq.sortOrder);
const [isSaving, setIsSaving] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSaving(true);
try {
await onSave({ question, answer, isActive, sortOrder });
} catch (error) {
console.error('Error saving FAQ:', error);
} finally {
setIsSaving(false);
}
};
// ... JSX del formulario
}Lo que esto demuestra en producción:
- TypeScript infiere el tipo de cada variable de estado del valor inicial pasado a
useState. No se necesita genérico explícito parauseState(faq.question)ya quefaq.questionya está tipado comostring. - El bloque
finallyasegura queisSavingse reinicie afalsetanto en éxito como en fallo. Sin él, un guardado fallido dejaría el formulario atrapado en un estado de carga. Partial<Faq>significa que solo los campos cambiados se envían al manejador de guardado, no el objeto FAQ completo. Esto mantiene la llamada API eficiente.- Cinco llamadas
useStateseparadas funcionan bien para un formulario de este tamaño. Para formularios con más de 6-8 campos, considerauseReducero una librería de formularios comoreact-hook-formpara reducir boilerplate y habilitar validación a nivel de campo. - El indicador
isSavingse usa para desactivar el botón de envío durante la operación asíncrona, evitando envíos duplicados.
Preguntas Frecuentes
¿Por qué debo usar la forma de función actualizadora setCount(prev => prev + 1) en lugar de setCount(count + 1)?
- Cuando llamas a
setCount(count + 1)múltiples veces en el mismo manejador de eventos, cada llamada lee el mismo valorcountobsoleto. - La forma actualizadora
prev => prev + 1siempre recibe el estado pendiente más reciente, por lo que tres llamadas resultan en +3 en lugar de +1. - También evita cierres obsoletos dentro de
setTimeout,useEffecty funciones asincrónicas.
¿Qué es un inicializador perezoso y cuándo debo usarlo?
- Un inicializador perezoso es una función pasada a
useState:useState(() => expensiveComputation()). - Se ejecuta solo en el primer renderizado (montar), no en cada re-renderizado.
- Úsalo cuando tu valor inicial requiera un cálculo costoso, como leer desde
localStorageo analizar datos grandes.
¿Cómo agrupa React múltiples llamadas setState?
- React agrupa todas las llamadas
setStatedentro del mismo manejador de eventos en un único re-renderizado. - En React 18+, el agrupamiento también se aplica a
setTimeout, promesas y manejadores de eventos nativos (agrupamiento automático). - Esto significa que llamar a
setA(1); setB(2);resulta en un re-renderizado, no dos.
¿Puedo almacenar un objeto en useState y actualizar solo una propiedad?
const [form, setForm] = useState({ name: "", email: "" });
// Correcto: expande para crear una nueva referencia
setForm(prev => ({ ...prev, name: "Alice" }));
// Incorrecto: mutar el objeto existente
form.name = "Alice"; // Sin re-renderizado¿Por qué mi componente no se re-renderiza cuando actualizo un objeto con los mismos valores?
- React usa
Object.ispara comparar el estado antiguo y el nuevo. - Si expandes el objeto (
{ ...obj }), creas una nueva referencia, que dispara un re-renderizado incluso si los valores son idénticos. - Si pasas la misma referencia de objeto exacta, React omite el re-renderizado.
Trampa: ¿Qué sucede si paso una función directamente en lugar de envolverla en una función de flecha para el inicializador perezoso?
useState(computeExpensive())llama a la función en cada renderizado y usa el resultado solo en el primero.useState(() => computeExpensive())llama a la función solo en el primer renderizado.- La primera forma desperdicia cálculo en cada re-renderizado.
¿Cómo tipifico useState con TypeScript cuando el valor inicial es null?
// Usa un genérico explícito para tipos unión
const [user, setUser] = useState<User | null>(null);
// Después, TypeScript sabe que user puede ser null
if (user) {
console.log(user.name); // estrechado a User
}¿Cómo tipifico un estado de array en TypeScript cuando empiezo con un array vacío?
// Sin el genérico, TypeScript infiere never[]
const [items, setItems] = useState<string[]>([]);
// Ahora puedes agregar strings
setItems(prev => [...prev, "new item"]);¿Cuándo debo usar useState frente a useReducer?
- Usa
useStatepara valores simples e independientes (un toggle, un contador, una entrada única). - Usa
useReducercuando tienes 3+ valores de estado relacionados, o cuando el estado siguiente depende tanto del estado actual como de un payload de acción. - Para formularios con más de 6-8 campos,
useReducero una librería de formularios reduce boilerplate.
Trampa: ¿Por qué llamar a setState dentro de una función render causa un bucle infinito?
- Llamar a
setStatedurante el renderizado programa un nuevo renderizado, que llama asetStatenuevamente, creando un bucle infinito. - React lanzará un error "Demasiados re-renderizados".
- Las actualizaciones de estado deben ocurrir en manejadores de eventos, efectos o callbacks -- nunca incondicionalmente durante el renderizado.
¿Es mejor tener un useState con un objeto o múltiples llamadas useState separadas?
- Múltiples llamadas
useStateson más simples y evitan expansiones innecesarias cuando solo un valor cambia. - Un estado de objeto único es mejor cuando los valores siempre cambian juntos (p. ej., coordenadas
{ x, y }). - Para formularios con más de 6 campos, considera
useReduceren lugar de cualquiera de los dos enfoques.
Relacionado
- useReducer — para lógica de estado compleja con acciones
- useRef — para valores mutables que no disparan re-renderizados
- Zustand Setup — cuando el estado necesita ser compartido globalmente