Módulo 6: Effects And Side Effects
Presentación del módulo: sincronizar con el mundo exterior
Por qué este módulo existe aquí
En el módulo 5 aprendiste la regla que ordena todo lo que has visto hasta aquí: deriva, no guardes. La lista de productos filtrada por la búsqueda no vive en un estado propio; se computa en el render a partir de products y query. Y detrás de esa regla hay una idea más profunda: el render es una función pura. Le entran los datos (props y estado), le sale la descripción de la UI, y en el camino no toca nada de afuera. No pide datos a un servidor, no escribe el título de la pestaña, no arranca un reloj. Esa pureza es lo que hace que UI = f(estado) sea confiable: para un estado dado, la UI siempre es la misma, sin sorpresas.
Pero una app de verdad tiene que tocar el mundo exterior. Los productos de Mercado no están escritos en el código: llegan de la red. El título de la pestaña del navegador —eso que aparece arriba, en la barra del navegador— debería decir el nombre del producto que estás viendo, y eso es escribir en el document. Una app de chat necesita conectarse a un servidor y quedarse escuchando. Un temporizador que actualiza "hace 3 minutos" necesita un reloj. Nada de eso lo puede hacer el render, porque el render es puro y todo eso es impuro: son side effects, efectos que ocurren a un lado del cálculo de la UI, sobre sistemas que viven fuera de React.
Conexión con el módulo. Aquí está la puerta que le faltaba a UI = f(estado): los efectos. Un efecto —que escribes con el hook useEffect— es el mecanismo de React para sincronizar tu componente con un sistema externo. Fíjate en el verbo: sincronizar, no transformar. Un efecto no calcula datos (eso lo hace el render, derivando); un efecto conecta el componente con algo de afuera —la red, el document, una conexión, un timer— y lo mantiene al día cuando el estado cambia. Toda la dificultad de useEffect, y casi todos sus bugs, salen de confundir esas dos cosas. Por eso el módulo tiene dos caras, y las dos importan igual: aprender a usar un efecto bien (para lo que de verdad es un sistema externo) y aprender a reconocer cuándo no necesitas ninguno (cuando lo que ibas a "sincronizar" en realidad se deriva en el render).
Este módulo trabaja cuatro piezas, y cada una se mide ejecutando código en Node. El ciclo de vida —render (describe, puro) → commit (React pinta) → effect (recién ahí sincroniza)—; el array de dependencias —[] una vez, [dep] cuando dep cambia, sin array cada render—; el cleanup —la función que el efecto devuelve para deshacer lo que montó—; y el fetch de datos con sus dos trampas clásicas —la race condition y el doble efecto de StrictMode—. Y una lección entera dedicada a la otra cara: cuándo no necesitas un efecto. Como en todo el curso, la sintaxis JSX real con useEffect se muestra en los bloques —es la API exacta que escribirás— y la lógica pura del ciclo se ejecuta en Node, con una cola manual determinista (nada de setTimeout real), para que cada salida sea reproducible letra por letra.
Una analogía: el aparato y el enchufe
Imagina que entras a una habitación y quieres usar un ventilador. Haces dos cosas: al entrar, lo conectas al enchufe (empieza a girar); y al salir, lo desconectas (deja de girar y de gastar luz). Esas dos acciones son inseparables: si conectas y nunca desconectas, el ventilador sigue girando en el cuarto vacío, gastando electricidad para nadie. Y si entras a otra habitación, primero desconectas el de acá y luego conectas el de allá; nunca dejas dos ventiladores girando a la vez por descuido.
Un efecto en React es exactamente ese par de acciones. El setup —el cuerpo del efecto— es conectar el aparato al enchufe: abrir la conexión, suscribirte al canal, arrancar el timer, pedir los datos. El cleanup —la función que el efecto devuelve— es desconectar al salir: cerrar la conexión, cancelar la suscripción, parar el timer. Y así como al cambiar de habitación desconectas antes de conectar, cuando una dependencia del efecto cambia, React corre tu cleanup (desconecta lo viejo) y luego tu setup de nuevo (conecta lo nuevo). Guarda esta imagen, porque resume el módulo entero: conectas al entrar, desconectas al salir, y al cambiar de cuarto desconectas antes de reconectar. El que conecta sin desconectar deja aparatos girando en cuartos vacíos —en React, eso se llama leak, y lo mediremos—.
Hay una segunda analogía que conviene tener a mano, para el array de dependencias: regar la planta cuando cambia la estación. No riegas en cada instante (eso sería sin array, cada render); no riegas una sola vez y nunca más (eso sería [], solo al montar); riegas cuando cambia la estación (eso es [season]: el efecto reacciona a un cambio específico). Y una tercera, para el cleanup: la suscripción a una revista que hay que cancelar cuando ya no la quieres, o te siguen llegando ejemplares (y cobrando) para siempre. Las tres apuntan a lo mismo: un efecto no es una instrucción que se dispara y se olvida; es una conexión que se mantiene y que hay que saber soltar.
El caso que nos acompaña: el storefront de Mercado, ahora conectado
Seguimos con el storefront de Mercado —la barra de búsqueda, la lista de productos, el carrito—. Hasta el módulo 5, los products estaban escritos como un array fijo en el código: la app funcionaba, pero los datos eran de mentira, clavados a mano. En una tienda real, los productos llegan de un servidor. Ese es el estreno de este módulo: el App de Mercado, al montarse, dispara un efecto que pide los productos a la red y, cuando llegan, los guarda en estado; la lista, que empezaba vacía (Loading...), se llena sola. Y hay un segundo efecto más fino: cuando el usuario escribe en la SearchBar y la búsqueda se hace en el servidor (no filtrando en el cliente, sino pidiéndole al servidor los resultados de ese query), cada cambio de query dispara una nueva petición —y aquí aparece la trampa que el módulo enseña a resolver: si dos peticiones vuelven fuera de orden, la respuesta vieja puede pisar a la nueva, y el cleanup es lo que lo evita—.
Recuerda los datos: un producto (Product) tiene id, name, priceCents (el precio en centavos, como entero: 2599), category e inStock. Al mostrarlo lo formateamos con formatPrice(2599) → "$25.99". Y recuerda el árbol del storefront, porque el efecto de carga vive en la raíz:
flowchart TD
App["App<br/>[effect: cargar products al montar]"] --> SearchBar[SearchBar]
App --> ProductList[ProductList]
App --> Cart[Cart]
ProductList --> PC1[ProductCard]
ProductList --> PC2[ProductCard]
Red["Red / servidor<br/>(sistema externo)"] -. respuesta .-> App
App -. fetchProducts .-> Red
Las flechas punteadas hacia la Red son la novedad del módulo: el App sale de React para hablar con un sistema externo. Eso no lo puede hacer el render; lo hace un efecto.
Ejemplo trabajado: el ciclo render → commit → effect
Antes de nada hay que ver cuándo corre un efecto dentro de la vida de un componente, porque de ahí sale todo lo demás. La secuencia tiene tres momentos y un orden fijo:
- Render. React ejecuta tu componente (una función). El componente describe cómo debe verse la UI. Es puro: no toca el DOM, no pide datos, solo devuelve la descripción.
- Commit. React toma esa descripción y escribe el DOM: la pantalla ya se ve.
- Effect. Después de que la pantalla se pintó, React corre tus efectos. Recién aquí el componente sincroniza con el mundo externo.
El punto clave —el que resuelve la mitad de las confusiones con useEffect— es que el efecto corre después del commit, no durante el render. Cuando tu efecto se ejecuta, el DOM ya existe y la pantalla ya se pintó. Veámoslo con un componente mínimo: una página de producto que, al montarse, sincroniza el título de la pestaña (document.title) con el nombre del producto. Primero, el componente tal como lo escribirás en React:
import { useEffect, useState } from 'react';
function ProductPage() {
const [productName] = useState('Wireless Mouse');
useEffect(() => {
document.title = productName; // sincroniza un sistema externo: el document
});
return <h1>{productName}</h1>;
}
Léelo con la analogía: document.title = productName es conectar el aparato —tocar algo que vive fuera de React (el document del navegador)—. Y fíjate en que eso no está en el cuerpo de la función que devuelve el <h1>; está dentro de useEffect, apartado, porque el render debe seguir siendo puro. El <h1> es la descripción (render); el document.title es el efecto (sincronización).
Ahora, ¿cómo lo ejecutamos si no hay navegador? Con el truco de siempre: modelamos las tres fases como funciones que imprimen su turno, y verificamos el orden. El render describe (y agenda el efecto), el commit "pinta", y solo después corre el efecto:
// Mini-runtime: una celda de estado + una ranura de effect.
// Modela el ciclo: render (describe, PURO) -> commit (pinta el DOM) -> effect (sincroniza).
let stateCell;
let firstRender = true;
let effectSlot; // { deps, cleanup }
let pendingEffect = null; // el effect agendado para correr TRAS el commit
function useState(initial) {
if (firstRender) stateCell = initial;
return [stateCell, (next) => { stateCell = next; renderAndCommit(); }];
}
function useEffect(setup) {
if (effectSlot === undefined) {
effectSlot = { cleanup: undefined };
pendingEffect = setup; // NO corre ahora: se agenda para despues del commit
}
}
// El componente: al montar, sincroniza document.title con el nombre del producto.
function ProductPage() {
const [productName] = useState('Wireless Mouse');
console.log(` [render] describo la UI: <h1>${productName}</h1> (funcion pura, NO toco el DOM)`);
useEffect(() => {
console.log(` [effect] sincronizo con afuera: document.title = "${productName}"`);
});
return { productName };
}
function renderAndCommit() {
ProductPage(); // 1) RENDER: describe y agenda el effect
firstRender = false;
console.log(' [commit] React escribe el DOM: la pantalla ya muestra el <h1>'); // 2) COMMIT
if (pendingEffect) { // 3) EFFECT: recien despues de pintar
const cleanup = pendingEffect();
effectSlot.cleanup = typeof cleanup === 'function' ? cleanup : undefined;
pendingEffect = null;
}
}
console.log('=== Ciclo de vida: render -> commit -> effect ===\n');
renderAndCommit();
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Ciclo de vida: render -> commit -> effect ===
[render] describo la UI: <h1>Wireless Mouse</h1> (funcion pura, NO toco el DOM)
[commit] React escribe el DOM: la pantalla ya muestra el <h1>
[effect] sincronizo con afuera: document.title = "Wireless Mouse"
Lee las tres líneas en orden, porque ese orden es la lección. Primero [render]: el componente describe su UI —el <h1>— y no toca nada de afuera. En medio de eso, la llamada a useEffect no ejecutó el efecto; solo lo agendó (por eso no aparece todavía el document.title). Luego [commit]: React escribe el DOM, la pantalla ya se ve. Y al final, [effect]: recién con el DOM ya pintado, corre el efecto y sincroniza el título de la pestaña. El efecto es lo último, no lo primero. Si tu efecto necesitara medir el ancho de un elemento del DOM, funcionaría, porque el DOM ya existe cuando el efecto corre. Y si por error hubieras escrito document.title = ... directamente en el cuerpo del render (fuera de useEffect), habrías roto la pureza: el render tocaría el mundo externo, y React no garantiza cuántas veces ni cuándo ejecuta un render. Por eso los side effects van en useEffect, apartados, y corren en su turno: después del commit.
El mapa del módulo
Guarda esta ruta; es cómo cada lección construye una parte de "efectos":
Tema Leccion Idea clave
──────────────────────────────── ──────── ──────────────────────────────────────────
Que es un side effect L2 el efecto SINCRONIZA con afuera (red, DOM,
conexion); el render DESCRIBE y es puro
El array de dependencias L3 [] una vez; [dep] cuando dep cambia;
sin array cada render (Object.is)
El cleanup L4 la funcion que el effect DEVUELVE; corre
antes del proximo setup y al desmontar
Fetch de datos L5 pedir al montar; products null -> datos;
el efecto es el lugar (la red es externa)
Race, cleanup y StrictMode L6 respuestas fuera de orden -> ignore; el
doble efecto de dev; por que existe React Query
Puede que no lo necesites L7 si se computa, DERIVA en el render; no
sincronices estado con estado via effect
──────────────────────────────── ──────── ──────────────────────────────────────────
Carga los productos de Mercado L8 el mini-proyecto, ejecutado
La frontera: qué NO entra en este módulo
Saber la frontera te evita esperar cosas que llegan en otra guía —y evita el error de usar useEffect para algo que ya tiene una herramienta mejor—.
- El fetch de datos en el servidor / SSR es la guía de Next.js (
nextjs-app-router). En este módulo pedimos datos desde el cliente, con un efecto, y la red la simulamos. En una app de producción, buena parte de los datos se piden en el servidor —antes de que la página llegue al navegador—, y eso cambia el juego: no hayuseEffect, no hay estado de carga, no hay race condition. Eso es Server Components y data fetching en el servidor, y es de la guía de Next.js. Aquí ves eluseEffectcrudo del lado del cliente, que es lo que necesitas entender primero. - Las librerías de datos —React Query, SWR— (caché, deduplicación, mutaciones, revalidación) son la guía de estado y datos (
frontend-state-and-data). En la lección 6 vas a sufrir en carne propia las trampas del fetch crudo (la race condition, el doble efecto, el manejo de carga y error a mano). Esas librerías existen justamente para que no tengas que escribir todo eso conuseEffect. Aquí verás el problema que resuelven; la solución, con su API, es la otra guía. - Levantar el estado y el estado compartido es el módulo 7. En el proyecto, el
Appes dueño de losproductscargados; cómo bajan a los hijos y cómo se comparte elcartentreProductCardyCartes levantar el estado. - Y todo lo del ecosistema que los módulos anteriores ya delimitaron sigue igual: HTML/CSS a fondo →
web-fundamentals-html-css; estilos/design systems →ui-systems-and-design-implementation; performance y deploy →fullstack-performance-and-deployment.
Errores comunes
Meter un side effect directamente en el cuerpo del render. Qué pasa: alguien escribe document.title = productName o hace un fetch sueltos, en el cuerpo del componente, fuera de useEffect. Por qué pasa: parece más directo —"lo pongo donde lo necesito"—. Cómo detectarlo: el efecto corre en momentos raros (dos veces, o en cada render), la pestaña parpadea, o la red recibe peticiones de más; y si tocas el DOM, a veces el elemento aún no existe. Cómo corregirlo: todo lo que toca el mundo externo va dentro de useEffect, para que corra en su turno (después del commit) y no rompa la pureza del render. El render describe; el efecto actúa.
Creer que useEffect es para transformar datos. Qué pasa: se usa un efecto para calcular la lista filtrada, el total del carrito, o el nombre completo a partir de dos campos —y guardarlo en otro estado—. Por qué pasa: se confunde "sincronizar" con "calcular". Cómo detectarlo: tienes un useEffect que solo lee estado, lo transforma y llama a un setState; hay dos piezas de estado donde una es siempre función de la otra. Cómo corregirlo: eso no es un efecto, es un derivado —se computa en el render (módulo 5)—. La lección 7 lo mide: el efecto que transforma provoca renders de más y bugs de desincronización. Regla: si no hay ningún sistema externo de por medio, probablemente no es un efecto.
Olvidar que el efecto corre después de pintar (esperar que corra "durante" el render). Qué pasa: alguien pone dentro del efecto algo que necesitaba antes del primer pintado (para evitar un parpadeo), y ve la UI mostrarse un instante con el valor viejo. Por qué pasa: se piensa el efecto como parte del render. Cómo detectarlo: un flash momentáneo (la pestaña dice "React App" y luego cambia al nombre real). Cómo corregirlo: acepta el orden —render → commit → effect— como es; para la mayoría de las sincronizaciones (título, análiticas, conexiones) está perfecto que ocurra después de pintar. Los casos que de verdad necesitan correr antes del pintado tienen otra herramienta, mencionada en la doc, que las guías posteriores cubren.
Ejercicios
Ejercicio 1 — ¿Efecto o no efecto? Para cada situación, di si necesita un useEffect (porque toca un sistema externo) o si no lo necesita (porque se puede derivar en el render o va en un event handler): (a) poner el título de la pestaña con el nombre del producto que se ve; (b) calcular el total del carrito sumando los precios de sus items; (c) enviar el pedido al servidor cuando el usuario hace clic en "Checkout"; (d) conectarse a un servidor de chat mientras el componente está montado; (e) mostrar la lista de productos filtrada por el query.
Ver solución
- (a) Efecto. El título de la pestaña es el
document.title, un sistema externo a React. Va en unuseEffectque sincronizadocument.titlecon el nombre del producto. (Lección 2.) - (b) No efecto — deriva. El total es una transformación de los items del carrito:
items.reduce(...). Se computa en el render; no hay nada externo. Guardarlo en estado y sincronizarlo con un efecto es el antipatrón de la lección 7. - (c) No efecto — event handler. "Cuando el usuario hace clic" describe una acción del usuario, no una sincronización continua. El envío va en el
onClickdel botón (módulo 4), no en un efecto. Un efecto es para lo que debe pasar porque el componente está en pantalla, no porque el usuario hizo algo. - (d) Efecto. Conectarte a un servidor de chat es sincronizar con un sistema externo que debe estar activo mientras el componente vive: setup = conectar, cleanup = desconectar. El caso de libro de
useEffectcon cleanup. (Lecciones 2 y 4.) - (e) No efecto — deriva. La lista filtrada se computa de
productsyqueryen el render (products.filter(...), módulo 5). No hay sistema externo; no va en un efecto.
La regla que las separa: ¿hay un sistema externo (red, DOM, conexión, timer)? Si sí, y debe mantenerse mientras el componente vive, es un efecto. Si es un cálculo, deriva. Si es una respuesta a una acción, es un handler.
Ejercicio 2 — El orden del ciclo. Con la analogía del aparato y el enchufe, explica por qué el efecto corre después del commit (después de que la pantalla se pintó) y no durante el render. ¿Qué problema traería tocar el document o pedir datos durante el render?
Ver solución
El render es como decidir qué aparato va en el cuarto: describes la UI, pero no conectas nada todavía. El commit es poner el cuarto tal como lo describiste (React escribe el DOM, la pantalla se ve). Y recién ahí, con el cuarto ya armado, conectas el aparato al enchufe (el efecto sincroniza con afuera). Tiene que ser en ese orden porque el efecto a veces necesita que el "cuarto" ya exista: si el efecto mide el ancho de un elemento del DOM, ese elemento debe estar pintado, y solo lo está después del commit.
Tocar el mundo externo durante el render trae dos problemas. Primero, rompe la pureza: el render debe poder ejecutarse cuantas veces React quiera, sin consecuencias visibles; si en medio pide datos o escribe el document, cada render dispararía peticiones o cambios sueltos. Segundo, el DOM aún no existe durante el render, así que cualquier efecto que dependa del DOM fallaría. Por eso React aparta los side effects en useEffect y los corre en su turno: al final, después de pintar.
Ejercicio 3 — Nombra el setup y el cleanup. Para una app de chat que se conecta a un servidor mientras el componente está montado, describe (en palabras, sin código) qué debería hacer el setup del efecto y qué debería hacer el cleanup. Luego di qué pasaría si el efecto no tuviera cleanup y el usuario navegara entre cinco salas de chat distintas.
Ver solución
- Setup (conectar el aparato): abrir la conexión al servidor de chat de la sala actual y empezar a escuchar mensajes.
- Cleanup (desconectar al salir): cerrar esa conexión, dejar de escuchar. Es la función que el efecto devuelve.
Si el efecto no tuviera cleanup y el usuario recorriera cinco salas, cada cambio de sala abriría una conexión nueva sin cerrar la anterior. Al final habría cinco conexiones abiertas a la vez —cinco aparatos girando en cuartos que ya dejaste—: el componente recibiría mensajes de salas viejas, gastaría recursos, y podría mostrar datos mezclados. Eso es un leak, y es exactamente lo que el cleanup evita: al cambiar de sala, primero desconecta la anterior (cleanup) y luego conecta la nueva (setup), dejando siempre exactamente una conexión activa. Lo mediremos en la lección 4.
Resumen y siguiente paso
En esta lección instalaste la última gran pieza de UI = f(estado): los efectos. Viste que el render es una función pura que no toca el mundo exterior, y que por eso React necesita una puerta aparte —useEffect— para sincronizar el componente con sistemas externos: la red, el document, una conexión, un timer. La palabra clave es sincronizar, no transformar: el render deriva los datos (módulo 5), el efecto alcanza lo de afuera. Lo mediste ejecutando el ciclo de vida —render → commit → effect— y comprobaste que el efecto corre último, después de que la pantalla ya se pintó. Y guardaste la analogía que resume el módulo: un efecto es conectar un aparato al enchufe al entrar y desconectarlo al salir —setup y cleanup, inseparables—.
Antes de avanzar deberías poder: definir un side effect y explicar por qué el render puro no puede tener ninguno; distinguir "sincronizar" (efecto) de "transformar" (derivar); nombrar el orden render → commit → effect y por qué el efecto va al final; y ubicar la frontera —qué es de este módulo (useEffect crudo del cliente) y qué es de otras guías (SSR en nextjs, React Query en frontend-state-and-data)—.
La lección 2 toma la primera pieza del mapa y la clava: qué es un side effect, con la definición operativa y el orden del ciclo medido en detalle. Ahí verás, ejecutado, por qué un efecto que escribe en el document es distinto de un cálculo que deriva un dato —y por qué confundirlos es la fuente número uno de bugs con useEffect—.
Recursos
- React, "Synchronizing with Effects" — react.dev/learn/synchronizing-with-effects. La página oficial que introduce
useEffectcomo una herramienta para sincronizar con sistemas externos; el punto de partida y la tesis de este módulo. En inglés. - React, "Adding Interactivity" — react.dev/learn/adding-interactivity. El índice de la sección donde vive todo esto (estado, eventos, efectos); útil para ver dónde encaja el módulo. En inglés.
- React, "You Might Not Need an Effect" — react.dev/learn/you-might-not-need-an-effect. La otra cara del módulo (lección 7): cuándo no usar un efecto. Léela pronto: evita la mitad de los
useEffectmal escritos. En inglés. - MDN, "Document: title property" — developer.mozilla.org/en-US/docs/Web/API/Document/title. El
document.titleque sincronizamos en el ejemplo: un sistema externo del navegador, por debajo de React. En inglés.