What is useEffect for and when does it run? What's the difference between passing `[]`, passing dependencies, or not passing an array?
Show answer — try answering out loud first
useEffect is for syncing the component with something external to React (timers, subscriptions, requests, the DOM). It runs AFTER React paints the render, and the dependency array controls when it repeats.
The render must be a pure function: it receives props and state, returns JSX. Everything that touches the outside world (a setInterval, an addEventListener, a fetch) is a side effect, and useEffect is the place for that.
The key points:
- It runs after the render, never during. First React updates the screen, then it runs your effects.
[](empty array): the effect runs only once, when the component mounts.[deps]: runs on mount and every time one of the dependencies changes between renders.- No array: runs after EVERY render. This is almost never what you want.
- Cleanup: if the effect returns a function, React runs it before repeating the effect and on unmount. That's where you clean up timers, subscriptions, and listeners to avoid memory leaks.
- It's not for everything. If a value can be computed from props or state (for example,
nombreCompletofromnombreandapellido), compute it directly in the render. Using an effect plus an extra state for that generates extra renders and sync bugs.
import { useEffect, useState } from "react";
type Props = { segundosLimite: number };
function Cronometro({ segundosLimite }: Props) {
const [segundos, setSegundos] = useState(0);
const [nombre, setNombre] = useState("Ana");
useEffect(() => {
// Runs on mount and every time segundosLimite changes
const id = setInterval(() => {
// Functional form: we don't need "segundos" as a dependency
setSegundos(prev => (prev < segundosLimite ? prev + 1 : prev));
}, 1000);
// Cleanup: React calls it before re-running the effect and on unmount.
// Without this, we would accumulate live intervals (memory leak).
return () => clearInterval(id);
}, [segundosLimite]);
// Derived state: computed in the render, does NOT need useEffect
const saludo = `Hola, ${nombre}. Llevas ${segundos}s`;
return (
<div>
<p>{saludo}</p>
<input value={nombre} onChange={e => setNombre(e.target.value)} />
</div>
);
}
Using useEffect to "copy" a derived value into another state:
// BAD: extra state + effect for something that computes itself
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(precio * cantidad);
}, [precio, cantidad]);
// Causes an extra render and can get out of sync
// GOOD: derive it directly in the render
const total = precio * cantidad;
Another classic stumble is forgetting the cleanup of a timer or listener: each re-render creates a new one and the previous one stays alive. The rule is simple: if the effect creates something that persists, the cleanup destroys it.
useEffect is for syncing the component with external systems: timers, subscriptions, requests, or browser APIs. It runs after React paints, and the dependency array controls the frequency: with an empty array it runs only on mount, with dependencies it runs when one of them changes, and with no array it runs on every render. If the effect creates something persistent, I return a cleanup function that React runs before repeating the effect and on unmount. And something I like to clarify: not everything goes in an effect; derived state is computed directly in the render, without extra state.
Create an AvisoOnline component hook that shows "Conectado" or "Sin conexión" by listening to window's online and offline events, with its correct cleanup.
See solution
import { useEffect, useState } from "react";
function AvisoOnline() {
const [conectado, setConectado] = useState<boolean>(navigator.onLine);
useEffect(() => {
const marcarOnline = () => setConectado(true);
const marcarOffline = () => setConectado(false);
window.addEventListener("online", marcarOnline);
window.addEventListener("offline", marcarOffline);
// Cleanup: we remove exactly the same listeners
return () => {
window.removeEventListener("online", marcarOnline);
window.removeEventListener("offline", marcarOffline);
};
}, []); // only on mount: the listeners live for the whole life of the component
return <p>{conectado ? "Conectado" : "Sin conexión"}</p>;
}