useState y useEffect: cómo usarlos correctamente
1. Descripción general
useState y useEffect son los dos hooks más básicos de React, pero también son donde más bugs surgen: componentes que se renderizan infinitamente, state que no se actualiza como esperas, effects que se ejecutan innecesariamente una y otra vez. Este artículo recorre el mecanismo real de funcionamiento de los dos hooks, los patrones correctos y los errores comunes que deberías evitar. Después de leerlo, tendrás más confianza a la hora de decidir “¿realmente necesito useEffect aquí?” — una pregunta que muchos desarrolladores de React aún responden incorrectamente.
2. useState básico
useState devuelve un par [value, setter]. Cada vez que llamas al setter, React programa un nuevo render del componente con el nuevo valor.
import { useState } from "react";
function CustomerCounter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Número de clientes: {count}</p>
<button onClick={() => setCount(count + 1)}>Añadir cliente</button>
</div>
);
}
useState(0)inicializacount = 0solo en el primer render.setCount(count + 1)crea un nuevo render concountincrementado en 1.countdentro del closure del render actual es immutable — no cambia por sí solo entre dos renders.
Nota: Llamar a
setCountno cambia el valor decountinmediatamente en el mismo render. React agrupa las actualizaciones y renderiza después.
3. useState: actualizar basándose en el state anterior
Si llamas al setter varias veces dentro del mismo event handler basándote en count (variable “congelada”), el resultado será incorrecto.
function handleTripleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// Resultado: count solo aumenta en 1, no en 3
}
Usa actualizaciones funcionales para obtener el valor más reciente:
function handleTripleClick() {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
// Resultado: count aumenta correctamente en 3
}
- Cuando pasas una función al setter, React garantiza que
prevsiempre es el valor más reciente del state en el momento del procesamiento, no el valor capturado en el closure antiguo. - Regla: si el nuevo state depende del state anterior, siempre usa actualizaciones funcionales.
4. useState: lazy initial state
Si el valor inicial requiere cálculos costosos, no llames a la función directamente — pasa la función a useState para que se ejecute solo una vez.
function parseInitialOrders(raw) {
console.log("Parseando datos de pedidos...");
return JSON.parse(raw);
}
function OrderList({ rawOrders }) {
// MAL: parseInitialOrders se ejecuta en cada render
// const [orders, setOrders] = useState(parseInitialOrders(rawOrders));
// BIEN: se ejecuta solo en el primer render
const [orders, setOrders] = useState(() => parseInitialOrders(rawOrders));
return <p>Número de pedidos: {orders.length}</p>;
}
- La forma incorrecta llama a
parseInitialOrdersen cada render, aunque React solo use el resultado del primero. - La forma correcta pasa una referencia de función; React solo la invoca cuando monta por primera vez.
5. useEffect básico
useEffect ejecuta un efecto secundario después de que el componente se renderice (después de que el DOM se haya actualizado).
import { useEffect, useState } from "react";
function PageTitleUpdater({ orderCount }) {
useEffect(() => {
document.title = `Tienes ${orderCount} pedidos`;
});
return <p>Pedido actual: {orderCount}</p>;
}
- Sin un dependency array (la segunda parte), este efecto se ejecuta después de cada render — tanto en mount como en update.
document.titlees un efecto secundario típico: interactúa con el mundo fuera de React (API del DOM), no es un cálculo de valor para renderizar.
6. useEffect: dependency array
El dependency array determina cuándo se ejecuta el efecto. Hay 3 casos que debes recordar bien.
6.1. Sin dependency array
useEffect(() => {
console.log("Se ejecuta después de CADA render");
});
6.2. Dependency array vacío []
useEffect(() => {
console.log("Se ejecuta UNA SOLA VEZ después del mount");
}, []);
6.3. Dependency array con valores
function CustomerProfile({ customerId }) {
const [customer, setCustomer] = useState(null);
useEffect(() => {
fetchCustomer(customerId).then(setCustomer);
}, [customerId]);
return <p>{customer?.name ?? "Cargando..."}</p>;
}
- El efecto solo se ejecuta nuevamente cuando
customerIdcambia entre dos renders (comparado usandoObject.is). - Si olvidas incluir
customerIden el array, el efecto usará el valor antiguo decustomerId(stale closure) — obtendrá el cliente incorrecto cuando el prop cambie.
Nota: El plugin ESLint
eslint-plugin-react-hookscon la reglaexhaustive-depste alertará cuando falte una dependencia. No desactives esta regla a menos que entiendas claramente las consecuencias.
7. useEffect: función de limpieza
Cuando un efecto crea una subscription, un timer o un event listener, debes devolver una función de limpieza para evitar pérdidas de memoria.
function OrderStatusListener({ orderId }) {
const [status, setStatus] = useState("pending");
useEffect(() => {
const subscription = subscribeToOrderStatus(orderId, setStatus);
return () => {
subscription.unsubscribe();
};
}, [orderId]);
return <p>Estado: {status}</p>;
}
- La función de limpieza se ejecuta antes de que el efecto se ejecute nuevamente (cuando
orderIdcambia) y cuando el componente se desmonta. - Si no limpias, cada vez que
orderIdcambie se creará una nueva subscription — eventualmentesetStatusserá llamado desde múltiples subscriptions antiguas, causando actualizaciones incorrectas o pérdidas de memoria.
Orden de ejecución cuando orderId cambia de 1 a 2:
1. Limpieza del efecto con orderId = 1
2. Nuevo efecto se ejecuta con orderId = 2
8. Errores comunes
8.1. Bucle infinito por faltar dependencias correctas
function BadExample() {
const [data, setData] = useState({ count: 0 });
useEffect(() => {
// nuevo object en cada render -> effect siempre ve "cambio" si pones data en deps
setData({ count: data.count + 1 }); // causa re-render -> effect se ejecuta de nuevo -> infinito
}, [data]);
return <p>{data.count}</p>;
}
Arregla usando actualizaciones funcionales y separando la condición de parada, o elimina useEffect completamente si esta lógica no necesita un efecto secundario real.
8.2. Stale closure en setTimeout/setInterval
function Countdown() {
const [seconds, setSeconds] = useState(10);
useEffect(() => {
const id = setInterval(() => {
setSeconds((prev) => prev - 1); // usa actualización funcional, NO uses seconds directamente
}, 1000);
return () => clearInterval(id);
}, []); // deps vacío porque no lees seconds directamente en el efecto
return <p>{seconds}</p>;
}
- Si escribes
setSeconds(seconds - 1)en lugar de usar actualización funcional, el closure siempre “recordará”seconds = 10del primer ejecutar del efecto — la cuenta regresiva se quedará atrapada en un valor incorrecto.
8.3. Usar useEffect para calcular un valor derivado
// MAL: innecesario, causa un render extra
function OrderSummaryBad({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
}, [items]);
return <p>Total: {total}</p>;
}
// BIEN: calcula directamente durante el render
function OrderSummaryGood({ items }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
return <p>Total: {total}</p>;
}
La versión “MAL” causa un render extra: renderiza con el total anterior, el efecto se ejecuta, setTotal dispara un segundo render con el valor correcto. La versión “BIEN” calcula durante el render, sin necesidad de efecto, sin state auxiliar.
9. Cuándo NO deberías usar useEffect
Nota:
useEffectes solo para sincronizar con sistemas fuera de React (DOM, API, subscriptions, timers). Si puedes calcular un valor directamente desde los props/state actuales, no uses un efecto.
Tres señales de que lo estás usando mal:
- Usas un efecto para calcular un valor derivado de otro state (usa una variable normal o
useMemo). - Usas un efecto para manejar eventos del usuario (manejalo directamente en el handler
onClick,onSubmit). - Usas un efecto para resetear state cuando un prop cambia — generalmente puedes usar el prop
keypara que React remonte el componente automáticamente en lugar de usar un efecto.
// En lugar de usar un efecto para resetear el formulario cuando customerId cambia:
// useEffect(() => { setForm(initialForm); }, [customerId]);
// Usa key para que React remonte el componente, así React resetea automáticamente todo el state interno:
<CustomerForm key={customerId} customerId={customerId} />
10. Escribir tests para el comportamiento del hook
Usa Jest con React Testing Library para verificar el comportamiento real en lugar de detalles de implementación.
npm install --save-dev @testing-library/react @testing-library/user-event jest
// CustomerCounter.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { CustomerCounter } from "./CustomerCounter";
test("incrementa count al hacer clic en el botón Añadir cliente", async () => {
const user = userEvent.setup();
render(<CustomerCounter />);
expect(screen.getByText("Número de clientes: 0")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Añadir cliente" }));
expect(screen.getByText("Número de clientes: 1")).toBeInTheDocument();
});
- El test llama a
rendery luego simula un clic real medianteuserEvent, como lo haría un usuario. - Las aserciones se basan en el contenido mostrado (
getByText), no en el state interno — esta es la forma correcta de probar en React: prueba el comportamiento, no la implementación.
PASS ./CustomerCounter.test.jsx
✓ incrementa count al hacer clic en el botón Añadir cliente (45 ms)
11. Conclusión
useState parece simple en la superficie pero es fácil cometer errores cuando olvidas que el valor en el closure está “congelado” en cada render — usa actualizaciones funcionales cuando el nuevo state dependa del anterior. useEffect es poderoso pero solo deberías usarlo para sincronizar con sistemas fuera de React; si un valor puede calcularse directamente durante el render, no lo envuelvas en un efecto. Dominar el dependency array y la función de limpieza te ayudará a evitar casi todos los bugs comunes relacionados con estos dos hooks.