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

  1. Què és un efecte i què no ho és
  2. La major part del codi no necessita useEffect
  3. Anatomia: funció d'efecte, neteja i dependències
  4. Les tres formes de l'array de dependències
  5. El cicle real d'execució
  6. StrictMode, la doble execució i la prova de la neteja
  7. Cas 1: un temporitzador de disponibilitat
  8. Cas 2: escoltar un esdeveniment del navegador
  9. Cas 3: carregar dades d'una API i la condició de carrera
  10. Cas 4: sincronitzar el títol del document
  11. L'array de dependències a fons
  12. Bucles infinits: causes reals i solucions de veritat
  13. Antipatrons que cal reconèixer
  14. useLayoutEffect i la càrrega de dades moderna

  1. 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.

  1. La major part del codi no necessita useEffect

Es 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.

  1. 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ÈNCIES

Les 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.

  1. 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.

  1. 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.

  1. StrictMode, la doble execució i la prova de la neteja

A 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-01

Molta 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; StrictMode nomé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à.

  1. 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, no estacio. L'objecte estacio podria 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ó.
  • setPlacesLliures no é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ó.

  1. 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.
  • removeEventListener necessita exactament la mateixa referència que es va passar a addEventListener. Escriure window.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.

  1. 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ó async retorna una promesa, i React espera que el valor retornat sigui la funció de neteja. Per això es declara carregar() a dins i se la crida; mai useEffect(async () => {…}, []).
  • fetch no llança error amb un 404 o un 500. Només llança si falla la xarxa. Cal comprovar resposta.ok a 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.

  1. 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]);

  1. 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-deps

Tracta'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.

  1. 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

// ❌ BUCLE claríssim
useEffect(() => {
  setComptador(comptador + 1);
}, [comptador]);

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

  1. 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);
}

  1. useLayoutEffect i la càrrega de dades moderna

Existeix 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 useEffect per transformar dades. El símptoma és un useState que 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 StrictMode perquè «l'efecte s'executa dues vegades». El problema és la neteja que falta, no StrictMode.
  • No comprovar resposta.ok. fetch considera 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.log al 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:

  1. 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.
  2. El primer efecte no té neteja: encara que tingués [], amb StrictMode quedarien dos temporitzadors, i en desmuntar el component el temporitzador continuaria viu cridant setHora sobre un component que ja no existeix.
  3. El segon efecte és estat derivat disfressat: horaFormatada es calcula íntegrament a partir de hora i zonaHoraria. 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

Mòdul 2: Components de React

Mòdul 3: Treballar amb Esdeveniments

Mòdul 4: Conceptes Avançats de Components

Mòdul 5: Hooks de React

Mòdul 6: Enrutament a React

Mòdul 7: Gestió de l'Estat

Mòdul 8: Optimització del Rendiment

Mòdul 9: Proves a React

Mòdul 10: Temes Avançats

Mòdul 11: Projecte: Construir una Aplicació Completa

© Copyright 2026. Tots els drets reservats