Efectos
En resumen: Un efecto es código para acciones secundarias (un temporizador, una petición, el título de la pestaña) que useEffect ejecuta después de dibujar y limpia antes de la siguiente ejecución y cuando el componente desaparece.
Problema
El banner ya no está, las comprobaciones sí
La banitsa de queso está agotada. El cliente pulsa «Vigilar el stock» y debajo del botón aparece un banner. Mientras el banner está abierto, la página comprueba el stock cada segundo. Aquí la comprobación solo escribe una línea en la consola.
Consola
Pulsa «Vigilar el stock», espera dos segundos y pulsa «Dejar de vigilar». El banner desaparece, pero «Comprobación n.º 3», «Comprobación n.º 4»… siguen llegando a la consola. Abre y cierra el banner otra vez: ahora hay dos líneas por segundo. El temporizador pertenece al navegador, no al componente. Nadie lo detuvo.
La idea
Dibujar solo calcula
Un componente dice qué debe haber en la pantalla: a partir de las props y el estado calcula el marcado. Eso es dibujar. La biblioteca lo repite cuando quiere y compara el resultado con el anterior.
Por eso dibujar tiene que ser puro: no hace nada salvo calcular el marcado. Un temporizador, una petición al servidor, un cambio de document.title, una suscripción a eventos de window hacen algo hacia fuera. Son efectos secundarios.
El cuerpo de la función StockWatcher es el dibujo. Se ejecuta en cada dibujo y, en el ejemplo de arriba, inicia un temporizador nuevo cada vez. Aquí no se nota, porque el banner se dibuja una sola vez. Pero si el temporizador cambia el estado (por ejemplo, un contador de comprobaciones en la pantalla), cada cambio dibuja de nuevo y cada dibujo inicia un temporizador más. Su número crece cada segundo.
Un efecto secundario necesita dos cosas que el dibujo no da: un momento para empezar y un momento para parar.
La idea
Efecto y limpieza
Un efecto es código que el componente ejecuta cuando ya está en la página. La limpieza es el código que detiene lo que se inició cuando el componente desaparece.
import { useEffect } from 'react';
export default function StockWatcher({ product }) { useEffect(() => { const id = setInterval(check, 1000); return () => clearInterval(id); }, [product]);
return <p role="status">Vigilamos el stock de {product.name}.</p>;}useEffect(función, dependencias)le dice a React: «ejecuta esta función cuando los cambios ya estén en el DOM».- La función que el efecto devuelve es la limpieza. React la llama cuando el componente sale de la página.
- El segundo argumento es la lista de dependencias: los valores que lee el efecto. La siguiente idea trata de ella.
La limpieza es el reflejo del efecto: setInterval y clearInterval, addEventListener y removeEventListener, una petición y la cancelación de esa petición.
Pruébalo
Detén las comprobaciones
El temporizador de StockWatcher arranca directamente en el componente y nadie lo detiene. Muévelo a un efecto que arranque cuando aparezca el banner y deténlo cuando el banner desaparezca. La comprobación abre el banner, espera «Comprobación n.º 1», lo cierra y se asegura de que «Comprobación n.º 2» nunca llegue.
Consola
Solución
App.jsximport { useState } from 'react';
import StockWatcher from './StockWatcher.jsx';
const PRODUCT = { id: 4, name: 'Banitsa de queso' };
export default function App() {
const [watching, setWatching] = useState(false);
return (
<main>
<h1>{PRODUCT.name}</h1>
<p>Agotado. Volverá pronto.</p>
<button type="button" onClick={() => setWatching(!watching)}>
{watching ? 'Dejar de vigilar' : 'Vigilar el stock'}
</button>
{watching && <StockWatcher product={PRODUCT} />}
</main>
);
}
StockWatcher.jsximport { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Comprobación n.º ${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return (
<p className="watcher" role="status">
Vigilamos el stock de {product.name}. Te avisaremos en cuanto vuelva.
</p>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
El temporizador va en useEffect. El efecto devuelve () => clearInterval(id) y las dependencias son [product]. La comprobación detecta un efecto sin return: el temporizador sigue ahí. También detecta return clearInterval(id); sin la flecha: entonces clearInterval se llama de inmediato, el temporizador se detiene antes de comprobar ni una vez, y React recibe undefined en lugar de una función.
La idea
Dependencias
Ahora el cliente puede elegir otro producto agotado mientras el banner está abierto. El efecto comprueba product. En cuanto el producto cambia, la comprobación antigua tiene que detenerse y una nueva tiene que empezar para el producto nuevo.
Para eso sirve la lista de dependencias. Después de cada dibujo, React compara cada valor de la lista con el de la vez anterior. Si al menos uno es distinto, primero llama a la limpieza de la ejecución antigua y luego ejecuta el efecto otra vez.
| El segundo argumento | Cuándo se ejecuta el efecto |
|---|---|
[product] |
después del primer dibujo y después de cada dibujo en que product sea distinto |
[] |
solo después del primer dibujo |
| ninguno | después de cada dibujo |
La regla: la lista contiene cada valor de props y estado que el efecto lee. Si falta product, el efecto se queda con el producto antiguo. La función del efecto se creó en el primer dibujo y recuerda los valores de entonces.
Míralo
La vida de un efecto
El banner se abre para la banitsa, el cliente elige los pepinos y luego cierra el banner.
function StockWatcher({ product }) { useEffect(() => { let checks = 0; const id = setInterval(() => { checks++; console.log(`Comprobación n.º ${checks}: ${product.name}`); }, 1000); return () => clearInterval(id); }, [product]);
return <p role="status">Vigilamos el stock de {product.name}</p>;}Paso a paso, en palabras
- Dibujo: React llama a StockWatcher y recibe el marcado. La función solo calcula qué mostrar. No inicia nada.
- El componente está en la página. Ahora React ejecuta el efecto: el temporizador arranca y cuenta las comprobaciones.
- El cliente elige otro producto. La prop cambia, StockWatcher se dibuja de nuevo y React compara [product] con la vez anterior: es distinto.
- Antes de la nueva ejecución llega la limpieza de la anterior: su temporizador se detiene. Sin ella, las comprobaciones antiguas seguirían junto a las nuevas.
- El efecto se ejecuta de nuevo con el producto nuevo. Su propio temporizador cuenta desde el n.º 1.
- El cliente cierra el banner: el componente sale de la página. Se ejecuta la última limpieza y el temporizador se detiene con ella.
- Cada ejecución tiene su pareja: lo que el efecto inicia, su limpieza lo detiene. Antes de la siguiente ejecución y cuando el componente se va.
El efecto se ejecuta dos veces y la limpieza se ejecuta dos veces: una antes de la nueva ejecución y otra cuando se retira el componente. Entre medias hay siempre exactamente un temporizador.
Pruébalo
Encuentra el error
Elige la línea con el error.
[] significa «solo después del primer dibujo». El efecto no se ejecuta otra vez para el producto nuevo, y su función recuerda el antiguo. Debe ser [product].Pruébalo
Completa
¿Qué va en el hueco para que el temporizador se detenga cuando el componente desaparece?
useEffect(() => {
const id = setInterval(check, 1000);
() => clearInterval(id);
}, [product]);return: la función que devuelve el efecto es la limpieza. React la llama antes de la siguiente ejecución y cuando se retira el componente.La idea
Cuándo no necesitas un efecto
Un efecto sirve para enlazar con algo de fuera del componente: un temporizador, un servidor, el título de la pestaña, window. Dos cosas parecen un efecto, pero no lo son.
- Un valor calculado a partir de otros. El total del carrito no se guarda en un estado aparte que un efecto actualiza. Se calcula al dibujar (o con
useMemo), como en la lección sobre estado derivado. Un efecto que lo escribe en un estado aparte es trabajo de más y un segundo lugar que puede desviarse del carrito. - Una respuesta a una acción. El pedido se envía porque el cliente pulsó «Pedir». Eso va en el manejador del clic, no en un efecto que vigila alguna bandera.
Lo que queda es el verdadero trabajo de un efecto: mantener el componente sincronizado con el mundo exterior mientras está en la página.
Más precisamente, para curiosos
La doble ejecución en desarrollo. En un proyecto nuevo de Vite, main.jsx envuelve <App /> en <StrictMode>. En desarrollo, StrictMode ejecuta adrede cada efecto, lo limpia y lo ejecuta otra vez, justo al mostrarse. Si la consola dice dos veces «el efecto arrancó», es una prueba, no un error: un efecto con una limpieza correcta la supera. El sandbox no usa StrictMode, así que aquí el efecto arranca una vez.
Por qué no useEffect(async () => …). Una función async devuelve una promesa, y React espera una función de limpieza o nada. La consola dice useEffect must not return anything besides a function, which is used for clean-up. La función async se escribe dentro del efecto y se llama allí. Eso viene en la siguiente lección.
Cómo se comparan las dependencias. Con Object.is, como con el estado. Un objeto creado de nuevo en cada dibujo es distinto cada vez, y el efecto se ejecuta cada vez. La regla «todo lo que lee el efecto está en la lista» la vigila el linter (react-hooks/exhaustive-deps), que forma parte de la plantilla de Vite.
Los componentes de clase tienen lo mismo en tres métodos: componentDidMount, componentDidUpdate y componentWillUnmount.
Ejercicio
Otro producto
Encima del banner hay un menú desplegable «Producto» con la banitsa y los pepinos. Ahora mismo StockWatcher inicia el temporizador una vez, cuando se muestra. Haz que, al cambiar el producto, la comprobación antigua se detenga y una nueva empiece desde el n.º 1 para el producto nuevo. La comprobación abre el banner, espera «Comprobación n.º 1» de la banitsa, elige los pepinos, espera «Comprobación n.º 1» de ellos, se asegura de que «Comprobación n.º 2» de la banitsa nunca llegue y cierra el banner.
Consola
Solución
App.jsximport { useState } from 'react';
import StockWatcher from './StockWatcher.jsx';
const PRODUCTS = [
{ id: 4, name: 'Banitsa de queso' },
{ id: 13, name: 'Pepinos' },
];
export default function App() {
const [productId, setProductId] = useState(4);
const [watching, setWatching] = useState(false);
const product = PRODUCTS.find((p) => p.id === productId);
return (
<main>
<label>
Producto{' '}
<select value={productId} onChange={(e) => setProductId(Number(e.target.value))}>
{PRODUCTS.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</label>
<h1>{product.name}</h1>
<p>Agotado. Volverá pronto.</p>
<button type="button" onClick={() => setWatching(!watching)}>
{watching ? 'Dejar de vigilar' : 'Vigilar el stock'}
</button>
{watching && <StockWatcher product={product} />}
</main>
);
}
StockWatcher.jsximport { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Comprobación n.º ${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return (
<p className="watcher" role="status">
Vigilamos el stock de {product.name}. Te avisaremos en cuanto vuelva.
</p>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
La lista de dependencias pasa a ser [product] y el efecto devuelve () => clearInterval(id). Ante un cambio, React llama primero a la limpieza del temporizador antiguo y luego ejecuta el efecto con el producto nuevo. La comprobación detecta [product] sin limpieza: entonces los dos temporizadores van a la vez.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Props: datos desde arriba · Condiciones: vacío, error, cargando · Estado · Estado derivado
- Continúa en: Datos de una API en un componente · Velocidad: cuándo se vuelve a renderizar un componente · Probar componentes · Del sandbox al proyecto
- Ejercicios sobre el tema: Efectos, datos y formularios