Por qué dejé de usar useEffect para sincronizar estado y qué uso ahora
Cometí un error que, sospecho, la mayoría de los equipos que trabajan con React sigue cometiendo hoy: usé useEffect como una herramienta de sincronización de estado de propósito general. Si algo cambiaba y yo quería reaccionar, ahí estaba el efecto. Limpio, familiar, y — lo entendí tarde — completamente equivocado para la mayoría de esos casos.
No lo cuento para flagelante. Lo cuento porque cuando revisé sistemáticamente los efectos de una codebase en React 19, encontré exactamente cuatro categorías de mal uso repetidas. Cada una tiene una solución mejor. Y ninguna de las cuatro requiere hacks ni librerías externas.
Mi tesis: useEffect no está roto. Lo que está roto es el modelo mental con el que lo enseñamos — como si fuera el lugar natural para "hacer cosas cuando algo cambia". React 19 pone herramientas mejores más cerca de la superficie, pero el criterio para elegir entre ellas sigue siendo necesario. Sin eso, React 19 solo te da nuevos lugares donde meter los mismos problemas.
useEffect para sincronizar estado: el antipatrón que nadie nombra
La documentación oficial de React tiene una página entera llamada — la frontera entre lo que corre en servidor y lo que corre en cliente cambia bastante qué patrones tienen sentido.
4. Transformaciones post-submit → Server Actions
La última categoría: el efecto que escucha el resultado de un submit para actualizar estado derivado, mostrar mensajes, o redireccionar.
// ❌ useEffect que observa el resultado de un submit
const [submitResult, setSubmitResult] = useState<Result | null>(null);
const [errorMessage, setErrorMessage] = useState('');
useEffect(() => {
if (submitResult?.error) {
setErrorMessage(submitResult.error.message);
}
}, [submitResult]);
Con Server Actions en React 19 y el hook useActionState (anteriormente useFormState), esto colapsa en un patrón mucho más directo:
// ✅ useActionState — el estado del form y el resultado de la acción, integrados
import { useActionState } from 'react';
async function submitForm(prevState: State, formData: FormData): Promise<State> {
'use server';
// La acción corre en el servidor, devuelve el nuevo estado
const result = await processForm(formData);
if (!result.ok) {
return { error: result.message };
}
return { success: true };
}
function MyForm() {
const [state, action, isPending] = useActionState(submitForm, { error: null });
return (
<form action={action}>
{/* Sin useEffect, sin estado extra, sin sincronización manual */}
{state.error && <p className="error">{state.error}</p>}
<button disabled={isPending}>Guardar</button>
</form>
);
}
El estado del form, el feedback de error y el loading están integrados sin un solo useEffect. No es magia — es que la responsabilidad está en el lugar correcto.
Los errores que persisten y los que desaparecen
Hay casos donde useEffect sí corresponde: suscripciones a stores externos, sincronización con APIs del DOM que no tenés control (un mapa de terceros, una librería de canvas), o setup/teardown de recursos que existen fuera del modelo de React.
Lo que desaparece con estos patrones:
Race conditions en fetch —use()y Server Components los eliminan estructuralmente
Renders intermedios inconsistentes — el estado derivado no tiene estado inconsistente porque no es estado
Grafos de efectos encadenados — cuando un efecto setea estado que dispara otro efecto, el debug es un infierno. Los event handlers lo cortan de raíz
Cleanup olvidado — si no hay efecto, no hay cleanup que olvidar
Lo que no desaparece:
Pensamiento sobre dependencias —useMemotambién tiene un array de dependencias.use()requiere entender cómo React maneja las promesas. El criterio sigue siendo necesario.
Complejidad de Suspense — boundaries mal ubicados rompen la UX de maneras no obvias. No es un reemplazo automático.
FAQ
¿useEffect quedó obsoleto en React 19?
No. El equipo de React no lo deprecó ni lo marcó como legacy. Lo que cambió es que React 19 pone herramientas mejores más a mano para los casos donde useEffect era la única opción disponible. Para suscripciones, integración con APIs del DOM externas, y sincronización con sistemas fuera del modelo de React, useEffect sigue siendo correcto.
¿use() reemplaza a useEffect para todos los fetches?
Para fetches en componentes cliente que dependen de interacción del usuario, use() con Suspense es una alternativa concreta. Para fetches de datos iniciales en App Router, Server Components son la respuesta más directa — ni use() ni useEffect. La elección depende de si el dato puede resolverse en servidor o necesita esperar al cliente.
¿Cuándo usar useMemo vs cálculo directo para estado derivado?
Cálculo directo primero. useMemo solo cuando el cálculo es mediblemente costoso (sort/filter sobre arrays grandes, transformaciones complejas) y el profiler confirma que hay un problema. Agregar useMemo preventivamente es otra forma de sobre-ingeniería — tiene un costo de legibilidad y las dependencias también se pueden equivocar.
¿useActionState funciona sin Server Actions?
Sí. useActionState acepta cualquier función asíncrona, no solo Server Actions. Si preferís manejar el submit del lado del cliente, podés pasarle una función client-side. La integración con Server Actions es la más limpia en Next.js con App Router, pero no es un requisito.
¿Cómo migro una codebase existente? ¿Hay un path incremental?
El path más seguro es por categoría, no por archivo. Empezá identificando todos los useEffect que solo derivan estado — son los más seguros de migrar y los que dan más beneficio inmediato. Después los que reaccionan a eventos del usuario. Los fetches últimos, porque requieren decisiones sobre Suspense boundaries que afectan la UX.
¿Estos patrones cambian si uso Zustand, Jotai o Redux?
Parcialmente. Los stores externos resuelven el problema del estado global, pero el antipatrón de derivar estado dentro de un componente con useEffect aparece igual. La pregunta "¿puedo calcular esto durante el render?" aplica independientemente del sistema de estado que usés.
Conclusión: el criterio que no viene del framework
Lo que más me frustró cuando revisé esta codebase no fue encontrar los antipatrones — eso era esperable. Fue darme cuenta de que estaban ahí porque en su momento nadie tenía una regla clara para decidir cuándo usar useEffect. Lo usábamos como herramienta por defecto para "hacer algo cuando algo cambia", y ese modelo mental es incorrecto desde el principio.
React 19 hace más fácil hacer lo correcto: use() existe, useActionState existe, los Server Components están más integrados. Pero sin el criterio de cuándo cada herramienta aplica, lo que pasa es que los mismos problemas migran a las nuevas APIs.
Mi postura concreta: antes de escribir un useEffect, preguntate si lo que querés hacer es (a) calcular algo a partir del estado existente, (b) reaccionar a una acción del usuario, (c) cargar datos, o (d) sincronizar con algo externo a React. Solo el último caso justifica un efecto. Los tres primeros tienen soluciones mejores en React 19, y la documentación oficial de React lo dice explícitamente.
Si estás revisando una codebase y no sabés por dónde empezar, el mismo ejercicio de auditoría sirve para otros patrones: el
SOCIAL SHARE CARD GENERATOR