Módulo 6: Effects And Side Effects
Qué es un side effect: sincronizar, no transformar
Descripción
Esta lección instala la definición que sostiene todo el módulo, y que si la tienes clara te ahorra la mitad de los bugs de useEffect: un efecto sincroniza el componente con un sistema externo; no transforma datos. Vamos a separar con precisión dos cosas que se parecen pero no lo son. Transformar es tomar el estado y las props y calcular algo nuevo —la lista filtrada, el total del carrito, el nombre formateado—: eso es puro, se hace en el render, y lo aprendiste en el módulo 5 (deriva, no guardes). Sincronizar es alcanzar algo que vive fuera de React —el document, la red, una conexión, el window— y mantenerlo al día cuando el estado cambia: eso es impuro, es un side effect, y va en un useEffect. Toda la lección gira sobre esa frontera, y sobre el orden en que el efecto corre dentro de la vida del componente.
Conexión con el módulo. Es la lección-cimiento. La 3 (dependencias), la 4 (cleanup), la 5 (fetch) y la 6 (race) son todas cómo se usa un efecto; esta es qué es y para qué sirve. Y la 7 —cuándo no necesitas un efecto— es el reverso exacto de lo que estableces aquí: si entiendes que un efecto sincroniza con lo externo, reconocerás de inmediato cuándo alguien está usando uno para transformar, que es el antipatrón. Todo se apoya en la pureza del render del módulo 5.
Una analogía: el aparato y el enchufe (otra vez, y más de cerca)
Vuelve al ventilador de la habitación. Hay una diferencia que ahora conviene afinar. Describir el cuarto —"aquí va un ventilador, allá una lámpara"— es una cosa: es pensar, planear, no cambia nada del mundo todavía. Conectar el ventilador al enchufe es otra: es una acción sobre algo real, la instalación eléctrica, que está fuera de tu cabeza. Puedes describir el cuarto mil veces sin que pase nada; pero cada vez que conectas algo al enchufe, el mundo cambia (empieza a girar, gasta luz).
El render es describir el cuarto: puro, sin consecuencias, lo puedes repetir cuantas veces quieras. El efecto es conectar al enchufe: una acción sobre un sistema externo (la instalación eléctrica = la red, el document, la conexión). Por eso los dos no pueden ir mezclados. Si conectaras el ventilador cada vez que piensas en el cuarto, tendrías un desastre eléctrico. React piensa en el cuarto (renderiza) muchas veces; por eso las conexiones al enchufe (los efectos) van apartadas, en useEffect, y corren en su turno controlado, no en cada pensamiento. Guarda la distinción: describir es gratis y puro (render); conectar toca el mundo y tiene consecuencias (efecto).
Qué cuenta como "sistema externo"
Un side effect es cualquier cosa que tu componente hace fuera de React —fuera del cálculo de la UI—. Los sospechosos habituales:
- La red. Pedir datos a un servidor (
fetch), enviar un evento de analítica. La lección 5 vive aquí. - El DOM del navegador que React no maneja. Escribir
document.title, poner el foco en un input, medir el tamaño de un elemento, escuchar el scroll o el resize delwindowconaddEventListener. - Suscripciones y conexiones. Un WebSocket de chat, un canal de precios en vivo, escuchar cambios de una API del navegador.
- Temporizadores.
setInterval,setTimeout—un reloj que actualiza "hace 3 minutos", un carrusel que avanza solo—. - Almacenamiento. Leer o escribir
localStorage.
Todos comparten una firma: existen aunque React no exista, y tu componente necesita conectarse a ellos mientras está en pantalla y desconectarse cuando se va. Eso es un efecto. Lo que no es un efecto: calcular la lista filtrada, el total, el formato del precio —eso es React puro, se deriva en el render—.
Ejemplo trabajado: el título de la pestaña, sincronizado
El ejemplo canónico de "sincronizar con un sistema externo" —el más simple que existe— es poner el título de la pestaña del navegador (document.title) igual al nombre del producto que se está viendo. El document es del navegador, no de React: es externo. Y "igual al nombre del producto" significa que cuando el producto cambie, el título debe cambiar con él: eso es sincronizar. Primero, el componente como lo escribirás en React:
import { useEffect, useState } from 'react';
function ProductPage() {
const [name, setName] = useState('Wireless Mouse');
useEffect(() => {
document.title = name; // sincroniza un sistema externo (el document) con el estado
});
return <h1>{name}</h1>;
}
Léelo con la distinción de la lección. El <h1>{name}</h1> es describir: puro, dice cómo se ve la UI. El document.title = name es conectar al enchufe: toca el document, algo externo. Por eso está dentro de useEffect y no suelto en el cuerpo. Ahora ejecutémoslo. Modelamos el document como un objeto externo (externalDocument), y corremos dos fases del ciclo: el montaje, y luego un cambio de estado (setName). En cada una imprimimos qué dice el título durante el render y después del efecto, para ver la sincronización pasar:
const externalDocument = { title: '(sin titulo)' }; // el sistema externo
let cell;
let mounted = false;
let pendingEffect = null;
function useState(initial) {
if (!mounted) cell = initial;
return [cell, (next) => { cell = next; renderAndCommit(); }];
}
function useEffect(setup) {
pendingEffect = setup; // se agenda; NO corre durante el render
}
let ui;
function ProductPage() {
const [name, setName] = useState('Wireless Mouse');
console.log(` [render] <h1>${name}</h1> (describe; document.title aun dice: "${externalDocument.title}")`);
useEffect(() => {
externalDocument.title = name; // sincroniza el sistema externo con el estado
console.log(` [effect] document.title := "${externalDocument.title}"`);
});
return { setName };
}
function renderAndCommit() {
ui = ProductPage();
mounted = true;
console.log(' [commit] pinta el DOM');
if (pendingEffect) { const fn = pendingEffect; pendingEffect = null; fn(); }
}
console.log('=== Un efecto sincroniza el document.title con el estado ===\n');
console.log('Montaje (name = "Wireless Mouse"):');
renderAndCommit();
console.log('\nEl estado cambia (setName("USB-C Hub")): re-render y el efecto re-sincroniza:');
ui.setName('USB-C Hub');
console.log(`\ndocument.title final (sistema externo): "${externalDocument.title}"`);
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Un efecto sincroniza el document.title con el estado ===
Montaje (name = "Wireless Mouse"):
[render] <h1>Wireless Mouse</h1> (describe; document.title aun dice: "(sin titulo)")
[commit] pinta el DOM
[effect] document.title := "Wireless Mouse"
El estado cambia (setName("USB-C Hub")): re-render y el efecto re-sincroniza:
[render] <h1>USB-C Hub</h1> (describe; document.title aun dice: "Wireless Mouse")
[commit] pinta el DOM
[effect] document.title := "USB-C Hub"
document.title final (sistema externo): "USB-C Hub"
Esta salida dice dos cosas, y las dos son la lección.
Primero, el orden: render → commit → effect. En el montaje, el [render] describe el <h1>Wireless Mouse</h1>, pero mira el detalle: durante ese render, document.title todavía dice "(sin titulo)" —el valor viejo—. El render no lo tocó; solo describió la UI. Luego el [commit] pinta. Y recién el [effect] pone document.title := "Wireless Mouse". El efecto es lo último, y es lo único que toca el sistema externo.
Segundo, sincronizar es mantener al día. Cuando el estado cambia con setName("USB-C Hub"), el ciclo se repite: el [render] describe <h1>USB-C Hub</h1>, pero fíjate otra vez —durante ese render, document.title aún dice "Wireless Mouse", el valor de la ronda anterior—. El título externo va un paso atrás del estado hasta que el efecto corre; y el efecto lo pone al día: document.title := "USB-C Hub". Eso es sincronizar: cada vez que el estado cambia, el efecto vuelve a correr y deja el sistema externo igual al estado. El título final del document es "USB-C Hub", en sintonía con lo que se ve. Si esto fuera una transformación (un cálculo), no habría "un paso atrás" ni sistema externo: el dato saldría del render directamente. La existencia de ese desfase —el sistema externo poniéndose al día después— es la firma de que estás ante un efecto, no ante un derivado.
Profundización: sincronizar vs transformar, la línea que no debes cruzar
La pregunta que decide todo. Antes de escribir un useEffect, hazte una sola pregunta: ¿hay un sistema externo de por medio? Si la respuesta es no —si lo único que haces es tomar estado y props y calcular un valor—, no es un efecto, es un derivado, y va en el render. Si la respuesta es sí —si tocas la red, el document, una conexión, un timer—, es un efecto, y va en useEffect.
Comparemos los dos, lado a lado, con Mercado:
function Storefront({ products }) {
const [query, setQuery] = useState('');
// TRANSFORMAR (derivar en el render): NO es un efecto. No hay nada externo.
const visibleProducts = products.filter((p) =>
p.name.toLowerCase().includes(query.toLowerCase())
);
// SINCRONIZAR (efecto): SI es un efecto. Toca el document, algo externo.
useEffect(() => {
document.title = `${visibleProducts.length} products found`;
}, [visibleProducts.length]);
return <ProductList products={visibleProducts} />;
}
visibleProducts se calcula en el render: es una transformación de products y query, pura, sin nada externo. No necesita efecto (y meterlo en uno sería el antipatrón de la lección 7). En cambio, poner ese conteo en el título de la pestaña sí toca el document, que es externo: eso sí es un efecto. La misma función tiene las dos cosas, cada una en su lugar. El error clásico es meter visibleProducts en un useEffect con un setState —transformar donde deberías derivar—; la lección 7 lo mide y muestra el costo (renders de más, bugs de desincronización).
Por qué el render debe seguir puro. React se reserva el derecho de ejecutar tus componentes cuando quiera, incluso más de una vez, para preparar la pantalla. Un render puro tolera eso sin problema: describir el cuarto diez veces da el mismo cuarto. Pero si el render tuviera un side effect —un fetch, un document.title = ... suelto—, ejecutarlo diez veces dispararía diez peticiones o diez escrituras. Por eso los side effects no van en el cuerpo del render; van en useEffect, que React corre en un turno controlado: una vez por commit (según sus dependencias, la lección 3), y siempre después de pintar.
El efecto corre después del commit — y qué implica. Como viste, cuando tu efecto se ejecuta, el DOM ya existe y la pantalla ya se pintó. Esto es bueno para casi todo: si tu efecto necesita medir un elemento del DOM, ya está ahí; si abre una conexión, la UI ya se ve mientras conecta. El "costo" es ese pequeño desfase que mediste: entre el commit y el efecto, el sistema externo aún tiene el valor viejo por un instante. Para el 99% de los casos (título, analítica, conexiones, fetch) es irrelevante. Los pocos casos que necesitan correr antes del pintado (para evitar un parpadeo visible al medir el layout) tienen otra herramienta que la doc menciona y las guías posteriores cubren; por defecto, useEffect es el correcto.
Errores comunes
Poner el side effect en el cuerpo del render, no en useEffect. Qué pasa: se escribe document.title = name; o un fetch(...) directamente en el cuerpo del componente, fuera de cualquier efecto. Por qué pasa: parece lo más directo. Cómo detectarlo: el efecto corre en cada render (la red recibe peticiones repetidas; la pestaña parpadea), y si tocas el DOM puede que el nodo aún no exista. Cómo corregirlo: mételo en useEffect(() => { ... }), para que corra en su turno controlado (después del commit) y no en cada render. El render describe; el efecto actúa sobre el exterior.
Usar un efecto para transformar (calcular un derivado). Qué pasa: se pone const [total, setTotal] = useState(0) y un useEffect que hace setTotal(items.reduce(...)). Por qué pasa: se confunde "cuando los items cambian, recalcula el total" con una sincronización. Cómo detectarlo: el useEffect no toca nada externo —solo lee estado, lo transforma y llama a un setState—. Cómo corregirlo: eso es un derivado; se computa en el render (const total = items.reduce(...)), sin estado extra y sin efecto. Regla: si no hay sistema externo, no es un efecto. (Lección 7.)
Creer que el efecto corre "durante" o "antes" del render. Qué pasa: alguien espera que el document.title esté puesto antes de que la UI se muestre, y se sorprende de ver un flash con el valor viejo. Por qué pasa: se piensa el efecto como parte del render. Cómo detectarlo: en la salida ejecutada, document.title aún dice el valor viejo durante el [render]; en una app real, un parpadeo momentáneo del título. Cómo corregirlo: acepta el orden render → commit → effect. El efecto va después de pintar, y para la sincronización de un título eso es perfectamente correcto: el usuario no percibe el desfase de milisegundos.
Ejercicios
Ejercicio 1 — Clasifica. Para cada línea de un componente, di si es transformar (va en el render, sin efecto) o sincronizar (va en un useEffect): (a) const total = items.reduce((s, it) => s + it.priceCents, 0); (b) document.title = product.name; (c) const label = formatPrice(product.priceCents); (d) localStorage.setItem('cart', JSON.stringify(cart)); (e) const inStock = products.filter((p) => p.inStock);
Ver solución
- (a) Transformar — render.
totalse calcula deitems; puro, sin nada externo. - (b) Sincronizar — efecto.
document.titlees eldocumentdel navegador, externo. - (c) Transformar — render.
formatPricees un cálculo puro sobrepriceCents. - (d) Sincronizar — efecto.
localStoragees almacenamiento del navegador, externo. - (e) Transformar — render.
inStockse deriva deproductsconfilter; puro.
La regla: (a), (c) y (e) no tocan nada de afuera → render. (b) y (d) tocan el navegador → efecto. La pregunta única: ¿hay un sistema externo?
Ejercicio 2 — Explica el desfase. En la salida del ejemplo trabajado, durante el segundo [render] el document.title "aun dice: Wireless Mouse" aunque el <h1> ya muestra "USB-C Hub". Explica por qué el título va un paso atrás en ese momento, y cuándo se pone al día.
Ver solución
El <h1> sale del render, que ya corrió con el estado nuevo (name = "USB-C Hub"): por eso describe <h1>USB-C Hub</h1> de inmediato. Pero el document.title no lo toca el render —lo toca el efecto, y el efecto corre después del commit—. Así que en el instante del render, el efecto de esta ronda todavía no se ejecutó; el título conserva el valor que le puso el efecto de la ronda anterior ("Wireless Mouse"). Se pone al día una línea más abajo, cuando corre el [effect] y hace document.title := "USB-C Hub".
Ese desfase —el sistema externo yendo un paso atrás hasta que el efecto lo alcanza— es la firma de que hay un efecto de por medio. Un derivado no tendría desfase: saldría del render junto con el <h1>.
Ejercicio 3 — El efecto suelto. Un compañero escribió esto y dice que "la pestaña parpadea y la analítica cuenta de más":
function ProductPage({ product }) {
document.title = product.name; // (1)
analytics.track('view', product.id); // (2)
const price = formatPrice(product.priceCents); // (3)
return <h1>{product.name} — {price}</h1>;
}
Di qué líneas están mal ubicadas y por qué, y reescribe el componente correctamente.
Ver solución
Las líneas (1) y (2) son side effects (tocan el document y el sistema de analítica, ambos externos) y están en el cuerpo del render. Por eso corren en cada render: la pestaña se reescribe una y otra vez (parpadeo) y la analítica cuenta una "vista" por cada render, no por cada visita real. La línea (3) está bien: formatPrice es una transformación pura, se queda en el render.
Corregido: los side effects van en useEffect, con las dependencias correctas (lección 3), para que corran cuando deben y no en cada render:
function ProductPage({ product }) {
const price = formatPrice(product.priceCents); // derivado: se queda en el render
useEffect(() => {
document.title = product.name; // sincroniza el titulo
}, [product.name]);
useEffect(() => {
analytics.track('view', product.id); // registra la vista una vez por producto
}, [product.id]);
return <h1>{product.name} — {price}</h1>;
}
Ahora el título se sincroniza solo cuando cambia product.name, y la analítica registra una vista solo cuando cambia product.id, no en cada render.
Resumen y siguiente paso
En esta lección clavaste la definición que sostiene el módulo: un efecto sincroniza el componente con un sistema externo; no transforma datos. Separaste transformar (tomar estado y props y calcular algo —puro, en el render, módulo 5—) de sincronizar (alcanzar la red, el document, una conexión, un timer —impuro, en useEffect—), con la pregunta única que las decide: ¿hay un sistema externo? Lo mediste ejecutando: el título de la pestaña se sincronizó con el estado en el orden render → commit → effect, y viste el desfase revelador —el sistema externo va un paso atrás hasta que el efecto lo alcanza—, que es la firma de que hay un efecto y no un derivado. Y guardaste por qué el render debe seguir puro: React lo ejecuta cuando quiere, así que los side effects van apartados.
Antes de avanzar deberías poder: definir un side effect y dar tres ejemplos de sistema externo; clasificar una línea como "transformar" o "sincronizar"; explicar el orden render → commit → effect y el desfase que produce; y reescribir un componente que tiene side effects sueltos en el render metiéndolos en useEffect.
La lección 3 responde la pregunta que quedó abierta: un efecto puede correr en cada render, pero casi nunca quieres eso. ¿Cuándo debe volver a correr? Esa es la función del array de dependencias —[] una vez, [query] cuando query cambia, sin array cada render—, y lo mediremos en una secuencia de renders para ver las tres formas en acción.
Recursos
- React, "Synchronizing with Effects" — react.dev/learn/synchronizing-with-effects. La página oficial:
useEffectcomo herramienta para sincronizar con sistemas externos, con el ejemplo deldocument.titley el orden del ciclo. La referencia central de esta lección. En inglés. - React, "Keeping Components Pure" — react.dev/learn/keeping-components-pure. Por qué el render debe ser puro y por qué los side effects van apartados; el fundamento de la frontera "transformar vs sincronizar". En inglés.
- React, "You Might Not Need an Effect" — react.dev/learn/you-might-not-need-an-effect. El reverso de esta lección (lección 7): cómo reconocer un efecto que en realidad debería ser un derivado. En inglés.
- MDN, "Document: title property" — developer.mozilla.org/en-US/docs/Web/API/Document/title. El
document.titledel ejemplo: una API del navegador, el sistema externo más simple con el que sincronizar. En inglés.