A 04-03 va quedar pendent una promesa: el model mental correcte per al que les classes anomenaven «cicle de vida» no és una seqüència de moments (componentDidMount, componentDidUpdate, componentWillUnmount), sinó la sincronització amb un sistema extern. Aquesta lliçó compleix aquesta promesa. useEffect és l'eina que connecta un component de React amb tot el que viu fora de React: temporitzadors, esdeveniments del navegador, el títol de la pestanya, una connexió de xarxa, un servidor. Però abans d'aprendre a fer-lo servir cal aprendre a no fer-lo servir, perquè useEffect és el hook pitjor emprat de l'ecosistema: s'hi fica lògica que pertany al render o a un gestor d'esdeveniments, i el resultat són renders de més, parpellejos, bucles infinits i errors impossibles de reproduir. Aquí veuràs la seva anatomia completa, quan s'executa de debò, per què StrictMode el crida dues vegades i per què això és bo, quatre casos legítims a CicloUrbano —inclosa la càrrega de dades amb condicions de carrera— i els antipatrons que has de reconèixer a la primera.
Contingut
- Què és un efecte i què no ho és
- La major part del codi no necessita
useEffect - Anatomia: funció d'efecte, neteja i dependències
- Les tres formes de l'array de dependències
- El cicle real d'execució
StrictMode, la doble execució i la prova de la neteja- Cas 1: un temporitzador de disponibilitat
- Cas 2: escoltar un esdeveniment del navegador
- Cas 3: carregar dades d'una API i la condició de carrera
- Cas 4: sincronitzar el títol del document
- L'array de dependències a fons
- Bucles infinits: causes reals i solucions de veritat
- Antipatrons que cal reconèixer
useLayoutEffecti la càrrega de dades moderna
- Què és un efecte i què no ho és
Comencem per la definició precisa, perquè la paraula «efecte» s'usa malament constantment.
Un efecte és un tros de codi que sincronitza el component amb un sistema extern a React, i que React executa després de pintar en pantalla, no durant el render.
Les dues parts importen. «Sistema extern» significa qualsevol cosa que React no controla:
| Sistema extern | Exemple a CicloUrbano |
|---|---|
| Temporitzadors del navegador | setInterval que refresca les places lliures d'una estació |
| API del DOM fora de l'arbre de React | document.title, localStorage, matchMedia |
| Esdeveniments globals | window.addEventListener('online', …) |
| Xarxa | fetch a l'endpoint de bicicletes |
| Biblioteques de tercers | Un mapa d'OpenStreetMap amb les estacions |
| Connexions persistents | Un WebSocket que avisa de bicicletes retornades |
I ara el que no és un efecte, que és on hi ha la majoria dels errors:
| Situació | És un efecte? | On va de debò |
|---|---|---|
| Filtrar la flota per tipus per mostrar-la | ❌ No | Durant el render, com a valor derivat |
| Calcular el preu total de la reserva | ❌ No | Durant el render, com a valor derivat |
| Enviar la reserva en prémer «Confirmar» | ❌ No | Al gestor onSubmit |
| Registrar l'analítica d'un clic | ❌ No | Al gestor onClick |
| Mostrar un avís en prémer un botó | ❌ No | Al gestor |
| Subscriure's a un temporitzador mentre el panell és visible | ✅ Sí | A useEffect |
| Carregar les estacions en mostrar la pantalla | ✅ Sí | A useEffect (o en un enrutador/biblioteca) |
La pregunta que separa un cas de l'altre és senzilla: això passa perquè l'usuari ha fet alguna cosa concreta, o perquè el component és en pantalla? Si és el primer, va en un gestor. Si és el segon, és un efecte.
- La major part del codi no necessita
useEffect
useEffectEs mereix un apartat propi perquè és el consell més rendible de tota la lliçó. Aquests són els tres casos en què la gent recorre a useEffect sense necessitar-lo.
Transformar dades per al render
// ❌ MALAMENT: un estat i un efecte per a alguna cosa que es calcula
function Cataleg({ bicicletes, tipusElegit }) {
const [visibles, setVisibles] = useState([]);
useEffect(() => {
setVisibles(
tipusElegit === 'todos'
? bicicletes
: bicicletes.filter((bicicleta) => bicicleta.tipus === tipusElegit)
);
}, [bicicletes, tipusElegit]);
...
}Aquest codi funciona, però provoca dos renders per cada canvi: un amb la llista vella i un altre, després de l'efecte, amb la nova. Durant una fracció de segon la pantalla mostra dades incorrectes. I si oblides una dependència, mostra dades incorrectes per sempre.
// ✅ BÉ: és un valor derivat, com a 04-01
function Cataleg({ bicicletes, tipusElegit }) {
const visibles =
tipusElegit === 'todos'
? bicicletes
: bicicletes.filter((bicicleta) => bicicleta.tipus === tipusElegit);
...
}Un render, sempre coherent, impossible de desincronitzar. Si el càlcul fos realment car, la solució no és un efecte sinó useMemo (08-03).
Respondre a un esdeveniment de l'usuari
// ❌ MALAMENT: l'efecte no sap PER QUÈ ha canviat l'estat
useEffect(() => {
if (reservaConfirmada) {
enviarAnalitica('reserva_confirmada');
}
}, [reservaConfirmada]);
// ✅ BÉ: l'analítica pertany al gestor que va provocar el fet
function gestionarConfirmacio({ bicicleta, hores, total }) {
enviarAnalitica('reserva_confirmada', { bicicletaId: bicicleta.id, hores, total });
setBicicletaSeleccionada(null);
}La versió amb efecte té una fallada greu: si l'estat reservaConfirmada torna a posar-se a true per un altre motiu —en recarregar un esborrany desat, per exemple—, l'analítica es dispara de nou. El gestor només s'executa quan algú prem el botó, que és justament el que volem mesurar.
Reiniciar l'estat en canviar una prop
// ❌ MALAMENT: render amb dades velles i després correcció
useEffect(() => {
setHores(1);
setErrors({});
}, [bicicleta.id]);
// ✅ BÉ: canvia la identitat del component amb key (05-01, apartat 10)
<PanellReserva key={bicicleta.id} bicicleta={bicicleta} />Regla que resumeix l'apartat: si ho pots calcular durant el render o fer-ho en un gestor, no és un efecte.
- Anatomia: funció d'efecte, neteja i dependències
useEffect(() => {
// 1. FUNCIÓ D'EFECTE: s'executa després del pintat
const connexio = crearConnexio(estacionId);
connexio.obrir();
// 2. FUNCIÓ DE NETEJA (opcional): desfà el que va fer l'efecte
return () => {
connexio.tancar();
};
}, [estacionId]); // 3. ARRAY DE DEPENDÈNCIESLes tres peces, una per una:
- La funció d'efecte conté el que cal fer per començar a sincronitzar. React l'executa després d'aplicar els canvis al DOM i que el navegador hagi pintat, així que mai bloqueja la interfície.
- La funció de neteja és el que retorna la funció d'efecte. Conté el necessari per deixar de sincronitzar. React l'executa abans de tornar a executar l'efecte i també en desmuntar el component. Si el teu efecte crea, obre, subscriu o arrenca alguna cosa, la neteja l'ha de destruir, tancar, desubscriure o aturar. És una simetria gairebé mecànica:
| Què fa l'efecte | Què ha de fer la neteja |
|---|---|
setInterval / setTimeout |
clearInterval / clearTimeout |
addEventListener |
removeEventListener |
connexio.obrir() |
connexio.tancar() |
fetch(...) |
controlador.abort() |
biblioteca.iniciar(node) |
biblioteca.destruir() |
Canviar document.title |
Restaurar el títol anterior, si escau |
- L'array de dependències li diu a React de quins valors depèn la sincronització. Si cap no ha canviat respecte al render anterior, React se salta l'efecte completament.
- Les tres formes de l'array de dependències
| Forma | Quan s'executa l'efecte | Quan s'executa la neteja | Ús típic |
|---|---|---|---|
useEffect(fn) — sense array |
Després de cada render | Abans de cada nova execució i en desmuntar | Gairebé mai. Sol ser un oblit |
useEffect(fn, []) — array buit |
Només després del primer render | Només en desmuntar | Subscripcions globals que no depenen de res |
useEffect(fn, [a, b]) — amb dependències |
Després del primer render i cada vegada que a o b canviïn |
Abans de cada reexecució i en desmuntar | El cas normal |
Exemples de les tres, amb el mateix component:
// Sense array: a cada render es crea un temporitzador nou. Gairebé segur un bug.
useEffect(() => {
console.log('S\'executa a TOTS els renders');
});
// Array buit: una subscripció global durant tota la vida del component
useEffect(() => {
function gestionarConnexio() { setEnLinia(navigator.onLine); }
window.addEventListener('online', gestionarConnexio);
window.addEventListener('offline', gestionarConnexio);
return () => {
window.removeEventListener('online', gestionarConnexio);
window.removeEventListener('offline', gestionarConnexio);
};
}, []);
// Amb dependències: la sincronització es refà quan canvia l'estació
useEffect(() => {
const connexio = crearConnexioDisponibilitat(estacionId);
connexio.obrir();
return () => connexio.tancar();
}, [estacionId]);La comparació de dependències es fa amb Object.is, és a dir, per referència per a objectes, arrays i funcions. Això és la causa del 90 % dels bucles infinits i ho tractem a l'apartat 12.
- El cicle real d'execució
Aquest diagrama substitueix per sempre la taula de mètodes del cicle de vida:
flowchart TD
A["Render: React crida el component"] --> B["Commit: React aplica els canvis al DOM"]
B --> C["El navegador pinta la pantalla"]
C --> D["React executa l'EFECTE"]
D --> E{"Nou render?"}
E -- "Les dependències NO canvien" --> F["No passa res:<br/>l'efecte se salta"]
F --> E
E -- "Les dependències SÍ que canvien" --> G["Executa la NETEJA de l'efecte anterior"]
G --> H["Executa l'EFECTE amb els valors nous"]
H --> E
E -- "El component es desmunta" --> I["Executa la NETEJA per última vegada"]
style D fill:#dcfce7
style G fill:#fde68a
style I fill:#fecaca
El que cal retenir: neteja i efecte van sempre aparellats. Mai s'executa un efecte nou sense haver netejat l'anterior. Per això el model mental de «muntar / actualitzar / desmuntar» és enganyós: des del punt de vista de useEffect no hi ha tres moments diferents, només hi ha començar a sincronitzar i deixar de sincronitzar, repetits tantes vegades com calgui.
Traduït a CicloUrbano: si el panell està connectat a l'estació est-01 i l'usuari canvia a est-02, React tanca la connexió amb est-01 i obre la de est-02. Aquest «canvi» no és un cas especial: és una aturada seguida d'una arrencada.
StrictMode, la doble execució i la prova de la neteja
StrictMode, la doble execució i la prova de la netejaA 01-03 vas veure que <StrictMode> munta cada component dues vegades en desenvolupament. Amb els efectes el comportament és encara més cridaner: React executa l'efecte, executa la seva neteja i torna a executar l'efecte, tot al muntatge inicial.
Consola en desenvolupament amb StrictMode:
Connexió oberta amb est-01
Connexió tancada amb est-01
Connexió oberta amb est-01Molta gent reacciona traient StrictMode. És exactament la reacció equivocada. El que React està fent és sotmetre el teu efecte a una prova: si el component es desmunta i es torna a muntar (una cosa que passa constantment en aplicacions reals en navegar entre rutes, a 06-01), queda tot al seu lloc?
- Si el teu efecte està ben escrit, la seqüència «efecte → neteja → efecte» deixa el sistema en el mateix estat que una sola execució. No notes res.
- Si li falta la neteja, la doble execució ho fa visible: dos temporitzadors corrent alhora, dues subscripcions, dues peticions. La fallada ja hi era abans;
StrictModenomés la treu a la llum en desenvolupament en lloc de en producció.
// ❌ Sense neteja: amb StrictMode veuràs DOS temporitzadors incrementant alhora
useEffect(() => {
setInterval(() => setSegons((previes) => previes + 1), 1000);
}, []);
// ✅ Amb neteja: la doble execució és indistingible d'una de sola
useEffect(() => {
const id = setInterval(() => setSegons((previes) => previes + 1), 1000);
return () => clearInterval(id);
}, []);En producció StrictMode no duplica res. La regla d'or: si treure StrictMode arregla el teu problema, el problema continua allà.
- Cas 1: un temporitzador de disponibilitat
Primer cas legítim. DisponibilitatEstacio consulta cada deu segons quantes places lliures té l'estació i ho mostra en pantalla.
// src/components/DisponibilitatEstacio.jsx
import { useState, useEffect } from 'react';
import estils from './DisponibilitatEstacio.module.css';
/**
* Places lliures d'una estació, refrescades periòdicament.
* Props:
* - estacio (objecte Estacio, obligatori)
* - intervalMs (nombre, opcional, per defecte 10000)
*/
function DisponibilitatEstacio({ estacio, intervalMs = 10000 }) {
const [placesLliures, setPlacesLliures] = useState(estacio.places);
const [ultimaLectura, setUltimaLectura] = useState(null);
useEffect(() => {
// COMENÇAR a sincronitzar amb el temporitzador del navegador
const identificador = setInterval(() => {
const lliures = consultarPlacesLliures(estacio.id);
setPlacesLliures(lliures);
setUltimaLectura(new Date());
}, intervalMs);
// DEIXAR de sincronitzar
return () => clearInterval(identificador);
}, [estacio.id, intervalMs]);
return (
<p className={estils.disponibilitat} aria-live="polite">
{estacio.nom}: {placesLliures} de {estacio.places} places lliures
{ultimaLectura && (
<span className={estils.marca}> · {ultimaLectura.toLocaleTimeString('ca-ES')}</span>
)}
</p>
);
}
export default DisponibilitatEstacio;Detalls que fan que aquest efecte sigui correcte:
- La dependència és
estacio.id, noestacio. L'objecteestaciopodria ser una referència nova a cada render del pare encara que les dades fossin idèntiques, i això reiniciaria el temporitzador constantment. L'identificador és una cadena: es compara per valor. intervalMsés a les dependències perquè el temporitzador en depèn. Si el pare canvia la freqüència, cal refer la sincronització.setPlacesLliuresno és a les dependències: els actualitzadors són estables (05-01, apartat 2).- La neteja cancel·la el temporitzador. Sense ella, canviar d'estació deixaria el temporitzador anterior corrent per sempre, escrivint en un component que ja mostra una altra estació.
- Cas 2: escoltar un esdeveniment del navegador
CicloUrbano ha d'avisar quan el dispositiu perd la connexió, perquè sense xarxa no es poden confirmar reserves.
// src/components/AvisConnexio.jsx
import { useState, useEffect } from 'react';
import Avis from './Avis.jsx';
function AvisConnexio() {
const [enLinia, setEnLinia] = useState(() => navigator.onLine);
useEffect(() => {
function gestionarEnLinia() { setEnLinia(true); }
function gestionarSenseLinia() { setEnLinia(false); }
window.addEventListener('online', gestionarEnLinia);
window.addEventListener('offline', gestionarSenseLinia);
return () => {
window.removeEventListener('online', gestionarEnLinia);
window.removeEventListener('offline', gestionarSenseLinia);
};
}, []);
if (enLinia) return null;
return (
<Avis to="advertencia">
Sense connexió. Pots consultar el catàleg, però no confirmar reserves.
</Avis>
);
}
export default AvisConnexio;Tres observacions:
- Les funcions gestores es declaren dins de l'efecte. Si fossin fora, al cos del component, serien referències noves a cada render i el linter exigiria incloure-les a les dependències, cosa que reiniciaria la subscripció sense motiu.
removeEventListenernecessita exactament la mateixa referència que es va passar aaddEventListener. Escriurewindow.removeEventListener('online', () => setEnLinia(true))no elimina res, perquè és una funció diferent. Aquest error deixa escoltadors acumulant-se i és una fuita de memòria clàssica.- L'estat inicial fa servir inicialització mandrosa (
() => navigator.onLine) per no consultar l'API del navegador a cada render.
- Cas 3: carregar dades d'una API i la condició de carrera
El cas més freqüent i el que més problemes dona. Comencem per la versió ingènua:
// ⚠️ Funciona… fins que l'usuari canvia ràpid d'estació
function PanellActivitat({ estacionId }) {
const [bicicletes, setBicicletes] = useState([]);
useEffect(() => {
fetch(`/api/estaciones/${estacionId}/bicicletas`)
.then((resposta) => resposta.json())
.then((dades) => setBicicletes(dades));
}, [estacionId]);
...
}El problema té nom: condició de carrera (race condition). Si l'usuari selecciona est-01 i de seguida est-02, es llancen dues peticions. Res garanteix que arribin en ordre.
sequenceDiagram
participant U as Usuari
participant C as Component
participant S as Servidor
U->>C: Selecciona est-01
C->>S: GET /api/estaciones/est-01/bicicletas
U->>C: Selecciona est-02 (ràpid)
C->>S: GET /api/estaciones/est-02/bicicletas
S-->>C: Resposta d'est-02 (arriba abans: és més lleugera)
Note over C: setBicicletes(bicis d'est-02) ✅
S-->>C: Resposta d'est-01 (arriba després: anava lenta)
Note over C: setBicicletes(bicis d'est-01) ❌
Note over U: La pantalla diu «est-02»<br/>però mostra les bicis d'est-01
Hi ha dues solucions, i el més idiomàtic és aplicar-les totes dues.
La bandera ignorar
Aprofita la neteja per marcar la resposta anterior com a obsoleta:
useEffect(() => {
let ignorar = false;
fetch(`/api/estaciones/${estacionId}/bicicletas`)
.then((resposta) => resposta.json())
.then((dades) => {
if (!ignorar) { // només actualitza si aquest efecte encara és vigent
setBicicletes(dades);
}
});
return () => { ignorar = true; }; // en canviar estacionId, invalida aquesta petició
}, [estacionId]);La clau està en el fet que cada execució de l'efecte té la seva pròpia variable ignorar, gràcies al tancament de JavaScript. Quan canvia estacionId, React executa la neteja de l'efecte vell, que posa el seu ignorar a true. Si aquesta resposta arriba tard, es descarta.
AbortController
La bandera evita l'error, però la petició continua viatjant i consumint xarxa. AbortController la cancel·la de veritat:
useEffect(() => {
const controlador = new AbortController();
fetch(`/api/estaciones/${estacionId}/bicicletas`, { signal: controlador.signal })
.then((resposta) => resposta.json())
.then((dades) => setBicicletes(dades))
.catch((error) => {
if (error.name !== 'AbortError') { // cancel·lar no és una fallada real
setError(error.message);
}
});
return () => controlador.abort();
}, [estacionId]);La versió completa que farem servir a CicloUrbano
// src/components/PanellActivitat.jsx
import { useState, useEffect } from 'react';
import LlistaBicicletes from './LlistaBicicletes.jsx';
import Avis from './Avis.jsx';
/**
* Bicicletes d'una estació, carregades des de l'API.
* Props:
* - estacionId (cadena, obligatori)
*/
function PanellActivitat({ estacionId }) {
const [bicicletes, setBicicletes] = useState([]);
const [estatCarrega, setEstatCarrega] = useState('inactiu');
const [error, setError] = useState(null);
useEffect(() => {
const controlador = new AbortController();
let ignorar = false;
async function carregar() {
setEstatCarrega('carregant');
setError(null);
try {
const resposta = await fetch(
`/api/estaciones/${estacionId}/bicicletas`,
{ signal: controlador.signal }
);
if (!resposta.ok) {
throw new Error(`El servidor ha respost ${resposta.status}`);
}
const dades = await resposta.json();
if (!ignorar) {
setBicicletes(dades);
setEstatCarrega('exit');
}
} catch (fallada) {
if (fallada.name === 'AbortError') return; // cancel·lació esperada
if (!ignorar) {
setError(fallada.message);
setEstatCarrega('error');
}
}
}
carregar();
return () => {
ignorar = true;
controlador.abort();
};
}, [estacionId]);
if (estatCarrega === 'carregant') return <p aria-live="polite">Carregant bicicletes…</p>;
if (estatCarrega === 'error') return <Avis to="error">No s'ha pogut carregar l'estació: {error}</Avis>;
return <LlistaBicicletes bicicletes={bicicletes} />;
}
export default PanellActivitat;Dos detalls tècnics que sovint es passen per alt:
- La funció d'efecte no pot ser
async. Una funcióasyncretorna una promesa, i React espera que el valor retornat sigui la funció de neteja. Per això es declaracarregar()a dins i se la crida; maiuseEffect(async () => {…}, []). fetchno llança error amb un 404 o un 500. Només llança si falla la xarxa. Cal comprovarresposta.oka mà, tal com fa l'exemple.- L'estructura d'estat segueix el principi 3 de 05-01:
estatCarregaés una cadena amb valors excloents, no tres booleans que es puguin contradir.
- Cas 4: sincronitzar el títol del document
El cas més petit i el més didàctic, perquè ensenya la paraula clau: sincronitzar.
// src/App.jsx (fragment)
const disponibles = bicicletes.filter((bicicleta) => bicicleta.estat === 'disponible').length;
useEffect(() => {
document.title = `CicloUrbano · ${disponibles} bicis disponibles`;
}, [disponibles]);No estem «executant codi en muntar»: estem declarant que el títol de la pestanya ha de reflectir el nombre de bicicletes disponibles, sempre. React s'encarrega de refer-ho quan aquest nombre canviï. Aquest canvi de perspectiva —de «quan s'executa» a «què s'ha de mantenir cert»— és el model mental que es va prometre a 04-03.
Si volguessis ser estricte, la neteja restauraria el títol original en desmuntar:
useEffect(() => {
const titolAnterior = document.title;
document.title = `CicloUrbano · ${disponibles} bicis disponibles`;
return () => { document.title = titolAnterior; };
}, [disponibles]);
- L'array de dependències a fons
Què entra a l'array
Tot valor reactiu que s'utilitzi dins de l'efecte. Un valor és reactiu si es declara dins del component i pot canviar entre renders: props, estat, i qualsevol variable o funció derivada d'ells.
| Valor utilitzat a l'efecte | Va a les dependències? | Motiu |
|---|---|---|
Una prop (estacionId) |
✅ Sí | Pot canviar a cada render |
Un estat (horesReserva) |
✅ Sí | Pot canviar |
Una constant derivada (const url = ...id) |
✅ Sí | Depèn de valors reactius |
L'actualitzador de useState |
❌ No | React garanteix que és estable |
El despatxar de useReducer |
❌ No | Estable (05-05) |
Un ref (laMevaRef) |
❌ No | L'objecte és estable (05-03) |
| Una constant fora del component | ❌ No | No és reactiva: mai canvia |
Una importació (bicicletes de domini.js) |
❌ No | Viu fora del component |
Per què mentir-li trenca coses
És temptador treure una dependència perquè l'efecte «deixi de disparar-se». Mai funciona: l'efecte continuarà fent servir el valor del render en què es va executar per última vegada, és a dir, un valor congelat (la instantània de 05-01).
// ❌ Mentida: l'efecte se subscriu a la primera estació i ja no canvia mai més
useEffect(() => {
const connexio = crearConnexioDisponibilitat(estacionId);
connexio.obrir();
return () => connexio.tancar();
}, []); // el linter avisa: falta 'estacionId'L'usuari canvia d'estació, la interfície mostra est-02 i les dades que arriben continuen sent les d'est-01. És exactament el tipus d'error que ningú reprodueix i que apareix en producció.
El linter
eslint-plugin-react-hooks (04-04) inclou la regla exhaustive-deps, que compara el contingut de l'efecte amb l'array i avisa del que falta:
React Hook useEffect has a missing dependency: 'estacionId'.
Either include it or remove the dependency array. react-hooks/exhaustive-depsTracta'l com un error, no com un suggeriment. I mai el silenciïs amb // eslint-disable-next-line react-hooks/exhaustive-deps: si l'avís et molesta, la solució no és callar-lo sinó canviar el codi perquè la dependència sobri.
- Bucles infinits: causes reals i solucions de veritat
Símptoma: la pestanya es congela, la consola escup milers de línies, o React llança «Maximum update depth exceeded». Sempre és la mateixa estructura: l'efecte canvia alguna cosa que és a les seves pròpies dependències.
Causa 1: un objecte o array recreat a cada render
// ❌ BUCLE: 'filtres' és un objecte NOU a cada render
function Cataleg({ tipus }) {
const filtres = { tipus, estat: 'disponible' };
useEffect(() => {
cercarBicicletes(filtres).then(setResultats);
}, [filtres]); // Object.is(objecteNou, objecteVell) === false, SEMPRE
}Cada render crea un objecte diferent; React veu una dependència canviada, executa l'efecte, l'efecte actualitza l'estat, això provoca un render, que crea un altre objecte… Solucions, per ordre de preferència:
// ✅ 1. Moure l'objecte DINS de l'efecte i dependre dels seus valors primitius
useEffect(() => {
const filtres = { tipus, estat: 'disponible' };
cercarBicicletes(filtres).then(setResultats);
}, [tipus]);
// ✅ 2. Dependre dels primitius directament
useEffect(() => {
cercarBicicletes({ tipus, estat }).then(setResultats);
}, [tipus, estat]);
// ✅ 3. Si l'objecte ve de fora i no el pots canviar, extreu el que necessites
const { tipus, estat } = filtres;
useEffect(() => {
cercarBicicletes({ tipus, estat }).then(setResultats);
}, [tipus, estat]);Causa 2: una funció recreada a cada render
// ❌ BUCLE: 'carregar' és una funció nova a cada render
function PanellEstacio({ estacionId }) {
function carregar() {
fetch(`/api/estaciones/${estacionId}`).then((r) => r.json()).then(setDades);
}
useEffect(() => { carregar(); }, [carregar]);
}
// ✅ Moure la funció dins de l'efecte: deixa de ser una dependència
function PanellEstacio({ estacionId }) {
useEffect(() => {
function carregar() {
fetch(`/api/estaciones/${estacionId}`).then((r) => r.json()).then(setDades);
}
carregar();
}, [estacionId]);
}Moure la funció dins de l'efecte és gairebé sempre la solució correcta, i té un avantatge afegit: el linter pot analitzar de veritat què fa servir la funció i calcular les dependències reals. useCallback també resol el cas, però és una eina de memoització que s'estudia a 08-03 i aquí seria fer servir una maça per matar una mosca.
Causa 3: l'efecte actualitza l'estat del qual depèn
Si l'objectiu és comptar alguna cosa, gairebé mai és feina d'un efecte. Si de debò ho fos, la forma funcional permet treure la dependència:
useEffect(() => {
setComptador((previ) => previ + 1);
}, [estacionId]); // depèn del fet que vols comptar, no del comptador
- Antipatrons que cal reconèixer
Actualitzar estat derivat amb un efecte
// ❌ El clàssic. Dos renders, risc de desincronització, cap avantatge
const [hores, setHores] = useState(2);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(hores * bicicleta.preuHora);
}, [hores, bicicleta.preuHora]);
// ✅ Un càlcul durant el render
const total = hores * bicicleta.preuHora;Encadenar efectes que es disparen entre si
// ❌ Quatre renders en cascada per a una sola acció de l'usuari
useEffect(() => {
if (bicicletaSeleccionada) setHores(1);
}, [bicicletaSeleccionada]);
useEffect(() => {
if (hores > 0) setTotal(hores * preu);
}, [hores, preu]);
useEffect(() => {
if (total > 0) setPotConfirmar(true);
}, [total]);Aquesta cadena és fràgil (canviar l'ordre la trenca), lenta (un render per esglaó) i impossible de seguir en llegir-la. La versió correcta fa tota la feina al gestor que va originar l'acció i deriva la resta:
// ✅ Una acció de l'usuari, un gestor, un render
function gestionarSeleccioBicicleta(bicicleta) {
setBicicletaSeleccionada(bicicleta);
setHores(1);
}
// Derivats, al cos del component
const total = bicicletaSeleccionada ? hores * bicicletaSeleccionada.preuHora : 0;
const potConfirmar = total > 0;Inicialitzar l'aplicació dins d'un efecte de component
// ❌ Amb StrictMode s'executa dues vegades, i no depèn del component
function App() {
useEffect(() => {
comprovarSessioUsuari();
carregarConfiguracioGlobal();
}, []);
}
// ✅ Fora de React, al mòdul: s'executa una vegada en carregar l'aplicació
if (typeof window !== 'undefined') {
comprovarSessioUsuari();
carregarConfiguracioGlobal();
}
function App() { … }Enviar dades en un efecte en lloc de al gestor
// ❌ Si l'estat es restaura per qualsevol motiu, la reserva s'envia una altra vegada
useEffect(() => {
if (dadesFormulari) enviarReserva(dadesFormulari);
}, [dadesFormulari]);
// ✅ L'enviament pertany al submit
function gestionarEnviament(esdeveniment) {
esdeveniment.preventDefault();
enviarReserva(dades);
}
useLayoutEffect i la càrrega de dades moderna
useLayoutEffect i la càrrega de dades modernaExisteix una variant, useLayoutEffect, que s'executa després del commit però abans que el navegador pinti; es reserva per mesurar el DOM i ajustar la posició d'alguna cosa sense que es vegi el salt (un tooltip que ha de cabre a la pantalla), i el seu ús indegut bloqueja el pintat, així que l'opció per defecte és sempre useEffect.
I un avís sobre l'apartat 9: encara que carregar dades amb useEffect és perfectament vàlid i convé entendre-ho a fons —és el que hi ha sota tota la resta—, en una aplicació de producció no s'escriu a mà. Falten la memòria cau entre pantalles, la deduplicació de peticions simultànies, els reintents, la revalidació en tornar a la pestanya i la invalidació després d'una escriptura. Tot això ho resolen biblioteques d'estat del servidor com TanStack Query o SWR, i també els carregadors de dades dels enrutadors (Mòdul 6) i dels frameworks com Next.js (Mòdul 10). És el tema de 07-06; aquí queda't amb el mecanisme, que és el que et permetrà entendre aquestes eines en lloc d'usar-les a cegues.
Errors Comuns i Consells
- Fer servir
useEffectper transformar dades. El símptoma és unuseStateque només s'actualitza dins d'un efecte. Gairebé sempre és un valor derivat disfressat. - Oblidar la neteja. Temporitzadors i escoltadors que sobreviuen al component són fuites de memòria i actualitzacions fantasma. Si l'efecte arrenca alguna cosa, la neteja l'atura.
- Passar una funció diferent a
removeEventListener. Desa la funció en una constant dins de l'efecte i fes servir aquesta mateixa referència a les dues crides. - Declarar la funció d'efecte com a
async. React interpretaria la promesa retornada com a funció de neteja. Declara una funció interna i crida-la. - Silenciar
exhaustive-deps. L'avís assenyala un problema real; amagar-lo el converteix en un error de producció. - Dependre d'un objecte o array creat al render. És la causa més comuna de bucle infinit. Depèn de primitius.
- Treure
StrictModeperquè «l'efecte s'executa dues vegades». El problema és la neteja que falta, noStrictMode. - No comprovar
resposta.ok.fetchconsidera un 500 una resposta perfectament vàlida. - Consell: abans d'escriure un efecte, digues en veu alta la frase «aquest component s'ha de mantenir sincronitzat amb ___». Si no la pots completar amb un sistema extern, no és un efecte.
- Consell: un
console.logal principi de l'efecte i un altre a la neteja, amb el valor de la dependència, resolen la majoria dels dubtes sobre quan s'executa què.
Exercicis
Exercici 1. Aquest RellotgeEstacio de CicloUrbano —el que va aparèixer a l'exercici 2 de 04-03— té tres fallades relacionades amb useEffect. Troba-les, explica què provoca cadascuna i escriu la versió corregida.
import { useState, useEffect } from 'react';
function RellotgeEstacio({ estacio, zonaHoraria }) {
const [hora, setHora] = useState(new Date());
const [horaFormatada, setHoraFormatada] = useState('');
useEffect(() => {
setInterval(() => setHora(new Date()), 1000);
});
useEffect(() => {
setHoraFormatada(hora.toLocaleTimeString('ca-ES', { timeZone: zonaHoraria }));
}, [hora, zonaHoraria]);
return <p>{estacio.nom}: {horaFormatada}</p>;
}Exercici 2. Escriu CercadorBicicletes amb retard (debounce): el component manté el seu estat local text (04-01) i ha d'avisar el pare amb alCercar(text) només quan l'usuari porti 400 ms sense escriure. Fes servir useEffect amb neteja. Explica per què la neteja és exactament el que produeix el retard.
Exercici 3. El component següent carrega la fitxa d'una estació i presenta una condició de carrera i una fuita. Reescriu-lo amb l'estructura d'estat de l'apartat 9 (una sola variable de fase), AbortController, bandera ignorar i gestió d'errors HTTP.
function FitxaEstacio({ estacionId }) {
const [estacio, setEstacio] = useState(null);
const [carregant, setCarregant] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setCarregant(true);
fetch(`/api/estaciones/${estacionId}`)
.then((r) => r.json())
.then((dades) => { setEstacio(dades); setCarregant(false); })
.catch((e) => setError(e.message));
}, [estacionId]);
if (carregant) return <p>Carregant…</p>;
if (error) return <p>{error}</p>;
return <h2>{estacio.nom}</h2>;
}Solucions
Solució 1.
Les tres fallades:
- El primer efecte no té array de dependències: s'executa després de cada render. Com que el mateix efecte actualitza
hora, cada tic provoca un render que crea un altre temporitzador. Al cap d'un minut hi ha desenes corrent alhora i el rellotge avança a salts. - El primer efecte no té neteja: encara que tingués
[], ambStrictModequedarien dos temporitzadors, i en desmuntar el component el temporitzador continuaria viu cridantsetHorasobre un component que ja no existeix. - El segon efecte és estat derivat disfressat:
horaFormatadaes calcula íntegrament a partir dehoraizonaHoraria. Sobra l'estat i sobra l'efecte; a més provoca un render extra per segon i fa que el primer render mostri una cadena buida.
// src/components/RellotgeEstacio.jsx
import { useState, useEffect } from 'react';
/**
* Props:
* - estacio (objecte Estacio, obligatori)
* - zonaHoraria (cadena, opcional)
*/
function RellotgeEstacio({ estacio, zonaHoraria }) {
const [hora, setHora] = useState(() => new Date());
useEffect(() => {
const identificador = setInterval(() => setHora(new Date()), 1000);
return () => clearInterval(identificador);
}, []); // el temporitzador no depèn de res: es crea una vegada
// DERIVAT: es recalcula a cada render, sense estat ni efecte
const horaFormatada = hora.toLocaleTimeString('ca-ES', { timeZone: zonaHoraria });
return <p>{estacio.nom}: {horaFormatada}</p>;
}
export default RellotgeEstacio;Solució 2.
// src/components/CercadorBicicletes.jsx
import { useState, useEffect } from 'react';
/**
* Cercador amb retard.
* Props:
* - alCercar (funció, obligatòria): rep el terme després de 400 ms d'inactivitat
* - retardMs (nombre, opcional, per defecte 400)
*/
function CercadorBicicletes({ alCercar, retardMs = 400 }) {
const [text, setText] = useState('');
useEffect(() => {
const identificador = setTimeout(() => alCercar(text), retardMs);
return () => clearTimeout(identificador);
}, [text, retardMs, alCercar]);
return (
<input
type="search"
value={text}
onChange={(esdeveniment) => setText(esdeveniment.target.value)}
aria-label="Cercar bicicletes per model"
/>
);
}
export default CercadorBicicletes;Per què la neteja produeix el retard: cada pulsació canvia text, així que React executa primer la neteja de l'efecte anterior —que cancel·la el temporitzador pendent— i després l'efecte nou, que en programa un altre a 400 ms. Mentre l'usuari continuï escrivint, cap temporitzador arriba a complir-se: cadascun mor cancel·lat per la tecla següent. Només quan passen 400 ms sense canvis sobreviu l'últim i crida alCercar. El retard no està programat enlloc: emergeix del parell efecte/neteja.
Nota: alCercar és a les dependències perquè el linter ho exigeix i és un valor reactiu. Si el pare la recrea a cada render, això reiniciaria el temporitzador constantment; es resol amb useCallback al pare (08-03) o, millor, extraient la lògica a un hook useDebounce, que és justament el que faràs a 05-06.
Solució 3.
function FitxaEstacio({ estacionId }) {
const [fase, setFase] = useState('carregant'); // 'carregant' | 'exit' | 'error'
const [estacio, setEstacio] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controlador = new AbortController();
let ignorar = false;
async function carregar() {
setFase('carregant');
setError(null);
try {
const resposta = await fetch(`/api/estaciones/${estacionId}`, {
signal: controlador.signal
});
if (!resposta.ok) throw new Error(`El servidor ha respost ${resposta.status}`);
const dades = await resposta.json();
if (!ignorar) {
setEstacio(dades);
setFase('exit');
}
} catch (fallada) {
if (fallada.name === 'AbortError') return;
if (!ignorar) {
setError(fallada.message);
setFase('error');
}
}
}
carregar();
return () => { ignorar = true; controlador.abort(); };
}, [estacionId]);
if (fase === 'carregant') return <p aria-live="polite">Carregant…</p>;
if (fase === 'error') return <p role="alert">{error}</p>;
return <h2>{estacio.nom}</h2>;
}Els problemes de l'original i el seu arranjament: no cancel·lava la petició anterior en canviar estacionId (condició de carrera); deixava carregant en true per sempre si la petició fallava, perquè el .catch no l'apagava (estat contradictori, principi 3 de 05-01); tractava un 404 com un èxit, perquè fetch no llança en respostes HTTP d'error; i podia intentar llegir estacio.nom amb estacio a null. La variable de fase única elimina d'arrel les combinacions impossibles.
Conclusió
useEffect no és «codi que s'executa en muntar»: és l'eina per sincronitzar un component amb un sistema extern, amb dues operacions simètriques —començar i deixar de sincronitzar— que React repeteix tantes vegades com calgui. Has vist la seva anatomia (efecte, neteja, dependències), les tres formes de l'array i quan es dispara cadascuna, el cicle real amb la neteja sempre aparellada a l'efecte, i per què la doble execució de StrictMode és una prova gratuïta que la teva neteja està ben feta. Amb CicloUrbano has escrit els quatre casos legítims: un temporitzador de disponibilitat, una subscripció a esdeveniments del navegador, la càrrega de dades amb AbortController i bandera ignorar contra la condició de carrera, i la sincronització del títol del document. I, sobretot, has après a no utilitzar-lo: transformar dades és render, respondre a un clic és gestor, reiniciar estat és key, i els efectes encadenats són una cascada de renders que gairebé sempre amaga un derivat mal plantejat.
Queda una peça que ha aparegut de reüll diverses vegades. El temporitzador de l'apartat 7 retornava un identificador que calia guardar; el cercador de l'exercici 2 necessitava recordar el temporitzador pendent; i a 03-05 es va esmentar una manera de llegir un camp del formulari accedint directament al node del DOM. Tot això són valors que cal recordar entre renders però que no han de provocar cap render, més la via d'escapament controlada de React cap als elements reals de la pàgina: donar el focus a un camp, mesurar un element, desplaçar una llista, obrir un <dialog>. La lliçó següent és Hook useRef i Accés al DOM.
Curs de React
Mòdul 1: Introducció a React
- Què és React?
- Configuració de l'Entorn de Desenvolupament
- Hola Món amb React
- JSX: Extensió de Sintaxi de JavaScript
- Com Renderitza React: Virtual DOM i Reconciliació
Mòdul 2: Components de React
- Entendre els Components
- Components Funcionals vs de Classe
- Props: Passar Dades als Components
- State: Gestió de l'Estat del Component
- Estils en els Components: CSS, Mòduls i Utilitats
Mòdul 3: Treballar amb Esdeveniments
- Gestió d'Esdeveniments a React
- Renderitzat Condicional
- Llistes i Claus
- Formularis i Components Controlats
- Validació de Formularis i Components No Controlats
- Accessibilitat en Components Interactius
Mòdul 4: Conceptes Avançats de Components
- Elevar l'Estat
- Composició vs Herència
- Mètodes del Cicle de Vida de React
- Hooks: Introducció i Ús Bàsic
- Límits d'Error: Capturar Fallades a la Interfície
Mòdul 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef i Accés al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalitzats
Mòdul 6: Enrutament a React
- Introducció a React Router
- Configuració de React Router
- Rutes Imbricades
- Navegació Programàtica
- Rutes Protegides i Control d'Accés
Mòdul 7: Gestió de l'Estat
- Introducció a la Gestió de l'Estat
- API de Context
- Redux: Introducció i Configuració
- Redux: Accions i Reductors
- Redux: Connectar-lo a React
- Estat del Servidor: Peticions, Memòria Cau i Sincronització
Mòdul 8: Optimització del Rendiment
- Tècniques d'Optimització del Rendiment a React
- Memoïtzació amb React.memo
- Hooks useMemo i useCallback
- Divisió de Codi i Càrrega Mandrosa
- Mesurar el Rendiment amb React DevTools Profiler
Mòdul 9: Proves a React
- Introducció a les Proves
- Proves Unitàries amb Jest
- Proves de Components amb React Testing Library
- Proves de Codi Asíncron i Simulació d'APIs
- Proves d'Extrem a Extrem amb Cypress
Mòdul 10: Temes Avançats
- Renderitzat al Servidor (SSR) amb Next.js
- Generació de Llocs Estàtics (SSG) amb Next.js
- Suspense i React Server Components
- TypeScript amb React
- React Native: Creació d'Aplicacions Mòbils
