La lliçó anterior va establir que un component es torna a executar per quatre motius, i que el segon —un ancestre s'ha tornat a renderitzar— és el responsable de la major part del treball que sobra en una aplicació React. També va establir que la primera resposta a aquest problema és estructural: baixar l'estat, passar children, paginar. Però hi ha casos on ja no queda marge estructural: PaginaCataleg s'ha de repintar quan canvia el terme de cerca, i amb ella es tornen a executar les 2.000 TargetaBicicleta del catàleg, incloses les 1.999 les dades de les quals són exactament les mateixes que fa un instant. Per a això existeix React.memo: un embolcall que li diu a React «abans d'executar aquesta funció, compara les seves props amb les de l'última vegada; si són iguals, no l'executis i reutilitza el resultat anterior». És una eina senzilla amb una API d'una sola línia, i tot i així és la més mal utilitzada de l'ecosistema, perquè la meitat dels memo que existeixen en producció no serveixen absolutament de res. En aquesta lliçó veuràs què fa exactament, com s'aplica al catàleg de CicloUrbano, per què falla tan sovint, què no bloqueja mai i quant costa posar-lo on no toca.

Contingut

  1. Què significa memoïtzar
  2. React.memo: signatura i comportament exacte
  3. L'arbre de render amb i sense memo
  4. Cas pràctic: TargetaBicicleta dins de LlistaBicicletes
  5. Per què memo sovint no serveix de res
  6. Diagnòstic: reproduir la fallada i veure-la
  7. El comparador personalitzat
  8. Signatura invertida respecte a shouldComponentUpdate
  9. Què bloqueja memo i què no bloqueja mai
  10. Quan aplicar-lo i quan no
  11. El cost de memo
  12. memo i el React Compiler

  1. Què significa memoïtzar

Memoïtzar (de l'anglès memoization) és guardar el resultat d'una operació associat a les seves entrades, per retornar el resultat guardat en lloc de repetir l'operació quan les entrades es repeteixen. És una tècnica general de programació, i la seva forma més simple no té res a veure amb React:

// Memoització manual d'una funció pura
function memoitzar(funcio) {
  const cache = new Map();

  return function (argument) {
    if (cache.has(argument)) {
      return cache.get(argument);       // no es recalcula res
    }
    const resultat = funcio(argument);
    cache.set(argument, resultat);
    return resultat;
  };
}

const calcularPreuTotal = memoitzar((hores) => hores * 2.5);

calcularPreuTotal(3);   // calcula: 7.5
calcularPreuTotal(3);   // retorna el guardat, sense calcular

D'aquí surten les dues condicions que fan que memoïtzar tingui sentit, i que valen igual per a React.memo:

  • La funció ha de ser pura: mateixes entrades, mateix resultat, sense efectes secundaris. Si no ho és, retornar un resultat guardat produeix un comportament incorrecte.
  • Recalcular ha de ser més car que comprovar i guardar. En l'exemple anterior, hores * 2.5 és més barat que consultar un Map: la memoització ho empitjora.

Un component de React és una funció pura de les seves props (aquesta és una de les regles de 04-04), així que és memoïtzable per definició. I la segona condició —que executar-lo sigui més car que comparar les seves props— és precisament el que cal verificar abans d'aplicar memo, i el que gairebé ningú verifica.

  1. React.memo: signatura i comportament exacte

import { memo } from 'react';

const Component = memo(function Component(props) {
  // …
});

memo rep un component i retorna un altre component amb el mateix comportament més una comprovació prèvia. La descripció precisa del que fa React quan arriba el torn de renderitzar un component embolcallat:

  1. Recupera les props del render anterior.
  2. Compara els dos objectes de props superficialment (shallow): recorre les claus de primer nivell i compara cada valor amb Object.is.
  3. Si totes són iguals, no crida la funció del component i reutilitza l'arbre d'elements que va produir l'última vegada.
  4. Si alguna difereix, executa el component amb normalitat.

Els dos adjectius d'aquesta comparació són l'origen de tots els problemes i de totes les solucions d'aquesta lliçó:

Superficial significa un sol nivell de profunditat.

const previes = { bicicleta: { id: 'bici-001', preuHora: 2.5 }, activa: false };
const noves  = { bicicleta: { id: 'bici-001', preuHora: 2.5 }, activa: false };

Object.is(previes.activa, noves.activa);        // true  → primitiu, mateix valor
Object.is(previes.bicicleta, noves.bicicleta);  // false → objectes diferents amb igual contingut
// Resultat: memo decideix que les props HAN canviat i renderitza

Per identitat significa Object.is, no igualtat estructural. Per a primitius (string, number, boolean, null, undefined) coincideix amb el que esperaries. Per a objectes, arrays i funcions, compara la referència en memòria, no el contingut.

Valor de la prop Object.is dona true amb el mateix contingut?
'bici-001' ✅ Sí
2.5 ✅ Sí
true ✅ Sí
{ id: 'bici-001' } ❌ No, si és un objecte nou
['urbana', 'electrica'] ❌ No, si és un array nou
() => reservar(id) ❌ No: cada render crea una funció nova
<EtiquetaEstat /> com a prop o children ❌ No: cada render crea un element nou

Aquesta taula és, a la pràctica, l'índex de la lliçó: la meitat inferior explica per què tants memo no funcionen.

  1. L'arbre de render amb i sense memo

flowchart TD
    subgraph SIN["SENSE memo: s'executa tot el subarbre"]
        A1["PaginaCataleg<br/>canvia el terme"] --> B1["CercadorBicicletes<br/>s'executa"]
        A1 --> C1["ResumFlota<br/>s'executa"]
        A1 --> D1["LlistaBicicletes<br/>s'executa"]
        D1 --> E1["TargetaBicicleta x 2.000<br/>s'executen TOTES"]
    end
flowchart TD
    subgraph CON["AMB memo: React compara abans de baixar"]
        A2["PaginaCataleg<br/>canvia el terme"] --> B2["CercadorBicicletes<br/>s'executa"]
        A2 --> C2{"ResumFlota memoitzat<br/>props iguals?"}
        C2 -->|Si| C3["NO s'executa<br/>es reutilitza l'arbre anterior"]
        A2 --> D2["LlistaBicicletes<br/>s'executa: la seva prop ha canviat"]
        D2 --> E2{"TargetaBicicleta memoitzada<br/>props iguals?"}
        E2 -->|"Si, en 1.988 targetes"| E3["NO s'executen"]
        E2 -->|"No, en 12 targetes"| E4["Només s'executen aquestes"]
    end

El detall important del segon diagrama: quan memo talla, talla el subarbre sencer. React no es limita a saltar-se ResumFlota; es salta també tot el que ResumFlota hauria renderitzat a dins. Per això el benefici de memo creix amb la mida del subarbre que protegeix, i per això posar-lo en una fulla barata rarament compensa.

  1. Cas pràctic: TargetaBicicleta dins de LlistaBicicletes

Aquest és l'estat actual del catàleg de CicloUrbano, amb el catàleg ampliat a 2.000 bicicletes.

// src/components/TargetaBicicleta.jsx (versió actual, sense memoïtzar)
import EtiquetaEstat from './EtiquetaEstat.jsx';
import estils from './TargetaBicicleta.module.css';

function TargetaBicicleta({ bicicleta, nomEstacio, alSeleccionar, alReservar }) {
  const formatador = new Intl.NumberFormat('ca-ES', {
    style: 'currency',
    currency: 'EUR'
  });

  return (
    <article className={estils.targeta}>
      <h3>{bicicleta.model}</h3>
      <p className={estils.estacio}>{nomEstacio}</p>
      <EtiquetaEstat estat={bicicleta.estat} />
      <p className={estils.preu}>{formatador.format(bicicleta.preuHora)} / hora</p>

      <button type="button" onClick={() => alSeleccionar(bicicleta.id)}>
        Veure fitxa
      </button>
      <button
        type="button"
        onClick={() => alReservar(bicicleta.id)}
        disabled={bicicleta.estat !== 'disponible'}
      >
        Reservar
      </button>
    </article>
  );
}

export default TargetaBicicleta;

Fixa't en el new Intl.NumberFormat(...) dins del cos: crear un formatador d'Intl és de les operacions més cares que es poden ficar en un render, de l'ordre de 0,05–0,1 ms cadascuna. Multiplicat per 2.000 targetes són entre 100 i 200 ms per cada pulsació de tecla al cercador. Aquest component no és barat.

Memoïtzar-lo és una línia:

// src/components/TargetaBicicleta.jsx (memoitzat)
import { memo } from 'react';
import EtiquetaEstat from './EtiquetaEstat.jsx';
import estils from './TargetaBicicleta.module.css';

// El formatador surt del component: és una constant, no depèn de res
const FORMAT_EURO = new Intl.NumberFormat('ca-ES', {
  style: 'currency',
  currency: 'EUR'
});

function TargetaBicicleta({ bicicleta, nomEstacio, alSeleccionar, alReservar }) {
  return (
    <article className={estils.targeta}>
      <h3>{bicicleta.model}</h3>
      <p className={estils.estacio}>{nomEstacio}</p>
      <EtiquetaEstat estat={bicicleta.estat} />
      <p className={estils.preu}>{FORMAT_EURO.format(bicicleta.preuHora)} / hora</p>

      <button type="button" onClick={() => alSeleccionar(bicicleta.id)}>
        Veure fitxa
      </button>
      <button
        type="button"
        onClick={() => alReservar(bicicleta.id)}
        disabled={bicicleta.estat !== 'disponible'}
      >
        Reservar
      </button>
    </article>
  );
}

export default memo(TargetaBicicleta);

Dos canvis, i l'ordre en què s'han fet no és casual:

  1. Treure Intl.NumberFormat fora del component. És l'optimització real: elimina 2.000 construccions d'objecte per render sense memoïtzar res, i continua funcionant encara que el memo no encerti mai. És la tècnica 3 de 08-01 —evitar el treball, no accelerar-lo— aplicada a una constant.
  2. Embolcallar l'exportació en memo. La convenció del projecte («export default per a components») es respecta: es memoïtza a l'exportació i la funció conserva el seu nom, que és el que veuràs a les eines de desenvolupament i al Profiler.

Fixa't en el detall d'anomenar la funció (function TargetaBicicleta(...)) en lloc d'usar una fletxa anònima. Si escrius export default memo(({ bicicleta }) => …), React DevTools mostrarà el component com Anonymous i el Profiler serà molt menys útil.

I ara la pregunta que ho decideix tot: amb aquest memo, quantes targetes es salten el render quan l'usuari escriu una lletra al cercador?

Cap. Ni una de sola. Vegem per què.

  1. Per què memo sovint no serveix de res

Aquest és el component pare tal com està escrit avui a CicloUrbano:

// src/components/LlistaBicicletes.jsx (el problema)
import { useNavigate } from 'react-router';
import TargetaBicicleta from './TargetaBicicleta.jsx';
import estils from './LlistaBicicletes.module.css';

function LlistaBicicletes({ bicicletes, estacions }) {
  const navegar = useNavigate();

  return (
    <ul className={estils.reixeta}>
      {bicicletes.map((bicicleta) => (
        <li key={bicicleta.id}>
          <TargetaBicicleta
            bicicleta={bicicleta}
            nomEstacio={
              estacions.find((est) => est.id === bicicleta.estacioId)?.nom ?? '—'
            }
            alSeleccionar={(id) => navegar(`/bicicletas/${id}`)}
            alReservar={(id) => navegar(`/reservas/nueva?bicicleta=${id}`)}
          />
        </li>
      ))}
    </ul>
  );
}

export default LlistaBicicletes;

Cada vegada que LlistaBicicletes s'executa, les funcions fletxa alSeleccionar i alReservar es creen de nou. Són funcions amb el mateix codi i el mateix comportament, però objectes diferents en memòria. Quan memo compara:

Object.is(propsPrevies.bicicleta, propsNoves.bicicleta);   // true  ✅ mateix objecte de la caché de Query
Object.is(propsPrevies.nomEstacio, propsNoves.nomEstacio); // true  ✅ string
Object.is(propsPrevies.alSeleccionar, propsNoves.alSeleccionar);   // false ❌ funció nova
// → memo conclou que les props han canviat i renderitza igualment

Basta una prop amb identitat inestable per anul·lar el memo completament. I no només no estalvia res: ara, a més d'executar les 2.000 targetes, React executa 2.000 comparacions de props que sempre fallen. El memo ha empitjorat la situació.

Aquest és el catàleg complet de props que trenquen memo, amb la seva forma habitual a CicloUrbano:

Prop inestable Exemple real Per què falla
Funció fletxa en línia alReservar={(id) => navegar(...)} Funció nova a cada render del pare
Objecte literal estil={{ margen: 8 }} Objecte nou a cada render
style en línia style={{ opacity: 0.5 }} El cas anterior, i el més freqüent de tots
Array literal tipus={['urbana', 'electrica']} Array nou a cada render
Resultat de .map/.filter visibles={bicicletes.filter(...)} Array nou encara que el contingut sigui idèntic
Element JSX icona={<EtiquetaEstat estat="libre" />} Element nou a cada render
children <Panell>{contingut}</Panell> children és una prop més, i gairebé sempre canvia d'identitat
Objecte construït al vol usuari={{ id, nom }} Objecte nou encara que id i nom no canviïn

Regla pràctica: memo només funciona si totes les props són primitius, o són objectes la identitat dels quals algú manté estable. A CicloUrbano, bicicleta és estable perquè ve de la caché de TanStack Query, que retorna els mateixos objectes mentre la dada no es revalidi. Les funcions no ho són, i aquí està la fallada.

La solució a aquest problema no és en aquesta lliçó: és a 08-03. useCallback estabilitza la identitat de les funcions i useMemo la dels objectes i arrays. memo i useCallback són dues meitats de la mateixa eina, i usar-ne una sense l'altra sol ser feina perduda. La lliçó següent tanca aquest cas amb el codi definitiu de LlistaBicicletes.

  1. Diagnòstic: reproduir la fallada i veure-la

Abans d'esperar a 08-05, hi ha una manera immediata de comprovar si un memo funciona, i consisteix a instrumentar el component temporalment:

// Instrumentació temporal, només per diagnosticar
function TargetaBicicleta({ bicicleta, nomEstacio, alSeleccionar, alReservar }) {
  console.count(`render TargetaBicicleta ${bicicleta.id}`);
  // …
}

console.count porta el compte per etiqueta. Escriu cinc lletres al cercador amb el catàleg de 2.000 bicicletes i mira la consola:

Situació Compte esperat després de 5 pulsacions
Sense memo Cada targeta arriba a 5 (o a 6 comptant el render inicial)
Amb memo i props inestables Exactament igual: el memo no talla res
Amb memo i props estables Cada targeta es queda en 1, tret de les que entren o surten del filtre

Un diagnòstic una mica més fi, que a més diu quina és la prop culpable:

// src/utilitats/depuracio.js
export function compararPropsAmbRastreig(nom) {
  return function (propsPrevies, propsNoves) {
    const claus = new Set([...Object.keys(propsPrevies), ...Object.keys(propsNoves)]);

    for (const clau of claus) {
      if (!Object.is(propsPrevies[clau], propsNoves[clau])) {
        console.log(`[${nom}] ha canviat la prop "${clau}"`, {
          abans: propsPrevies[clau],
          ara: propsNoves[clau]
        });
      }
    }
    return false;   // false = "no son iguals" → deixa que renderitzi; només observem
  };
}

// Ús TEMPORAL, mai en producció:
// export default memo(TargetaBicicleta, compararPropsAmbRastreig('TargetaBicicleta'));

Amb la LlistaBicicletes actual, la consola s'omple de ha canviat la prop "alSeleccionar" i ha canviat la prop "alReservar", que és el diagnòstic exacte. Recorda treure-ho després: recórrer les claus de les props de 2.000 components a cada render és justament el tipus de cost que aquest mòdul intenta eliminar.

  1. El comparador personalitzat

memo accepta un segon argument: una funció que decideix la comparació en el teu lloc.

memo(Component, (propsPrevies, propsNoves) => boolean)

El contracte és precís:

  • Retorna true si consideres que les props són iguals → React no renderitza.
  • Retorna false si són diferents → React renderitza.

Un ús legítim a CicloUrbano: TargetaEstacio rep l'objecte complet de l'estació, però només pinta el nom, el barri i el nombre de places lliures. Si l'objecte porta a més un historial d'incidències que canvia constantment sense afectar el que es veu, la comparació per defecte renderitzaria en va.

// src/components/TargetaEstacio.jsx
import { memo } from 'react';

function TargetaEstacio({ estacio, placesLliures, alObrir }) {
  return (
    <article>
      <h3>{estacio.nom}</h3>
      <p>{estacio.barri}</p>
      <p>{placesLliures} de {estacio.places} places lliures</p>
      <button type="button" onClick={() => alObrir(estacio.id)}>Veure detall</button>
    </article>
  );
}

// Només importen tres camps i la funció; la resta de l'objecte és soroll
function sonEquivalents(previes, noves) {
  return (
    previes.estacio.id === noves.estacio.id &&
    previes.estacio.nom === noves.estacio.nom &&
    previes.estacio.places === noves.estacio.places &&
    previes.placesLliures === noves.placesLliures &&
    previes.alObrir === noves.alObrir
  );
}

export default memo(TargetaEstacio, sonEquivalents);

Els riscos, que són seriosos i expliquen per què s'utilitza poc:

  • És una promesa que tu mantens. Si demà la targeta comença a mostrar estacio.barri i ningú actualitza el comparador, el barri no s'actualitzarà mai en pantalla. És una fallada silenciosa, difícil de reproduir i que el linter no detecta.
  • children gairebé sempre l'invalida. Si el component rep children, comparar-los correctament és pràcticament impossible.
  • Comparar en profunditat sol sortir més car que renderitzar. Un JSON.stringify(previes) === JSON.stringify(noves) sobre un objecte de mida mitjana costa més que executar un component senzill, i a més falla amb funcions, dates i undefined.
// ❌ Antipatró clàssic
export default memo(Targeta, (a, b) => JSON.stringify(a) === JSON.stringify(b));

Regla: si necessites un comparador personalitzat, gairebé sempre el veritable problema és que el component rep props que no necessita. Passar nom, barri i placesLliures com a primitius en lloc de l'objecte sencer elimina el problema sense comparador i sense memo.

  1. Signatura invertida respecte a shouldComponentUpdate

A 04-03, en recórrer el cicle de vida heretat, va aparèixer shouldComponentUpdate com el seu antecessor en components de classe. La comparació és útil, i la seva diferència més perillosa és el sentit del valor retornat.

shouldComponentUpdate(nextProps, nextState) memo(C, (propsPrevies, propsNoves))
On viu Mètode d'una classe Embolcall d'una funció
Pregunta que respon «He d'actualitzar?» «Són iguals?»
true significa Renderitza No renderitzis
false significa No renderitzis Renderitza
Accés a l'estat Sí, compara també nextState No: només props
Versió mandrosa PureComponent (comparació superficial) memo sense segon argument

La confusió és tan habitual que val la pena fixar-la amb una frase: shouldComponentUpdate respon a «actualitza», memo respon a «són iguals». Retornar true al lloc equivocat produeix el pitjor error possible en una interfície: un component que deixa d'actualitzar-se, sense cap missatge d'error, i que només es descobreix quan algú s'adona que l'estat d'una bicicleta s'ha quedat congelat en disponible.

I una diferència de fons: memo no veu l'estat. Cosa que porta directament a l'apartat següent.

  1. Què bloqueja memo i què no bloqueja mai

Aquest és l'apartat que més malentesos evita. memo intervé en una sola de les quatre causes de render de 08-01.

Causa del render La bloqueja memo? Explicació
Un ancestre s'ha renderitzat i les props no han canviat És exactament la seva funció, i l'única
Un ancestre s'ha renderitzat i alguna prop ha canviat d'identitat ❌ No La comparació falla i renderitza
El seu propi estat canvia (useState, useReducer) Mai Un component sempre es renderitza quan el seu estat canvia
Un context que consumeix canvia (useContext) Mai La subscripció al context és interna i memo no la veu
Un magatzem extern subscrit canvia (useSelector, useQuery) Mai Igual: la subscripció és dins del component
Una key diferent ❌ No Canviar la clau desmunta i remunta: no hi ha props prèvies a comparar

Els dos «mai» del centre són els importants, i el del context és el que més codi inútil ha generat al món:

// ❌ Aquest memo NO evita res
const BotoTema = memo(function BotoTema() {
  const { tema, alternarTema } = useTema();   // consumeix context
  return <button onClick={alternarTema}>Tema: {tema}</button>;
});

BotoTema no rep props, així que la comparació de memo sempre dona «iguals»… i tot i així el component es torna a executar cada vegada que canvia el valor de ProveidorTema, perquè consumir un context és una subscripció, no una prop. El memo aquí només afegeix una comparació buida a cada render del pare. El que sí que funciona per a aquest problema és el de 07-02: dividir el context en estat i accions, i estabilitzar el seu value (08-03).

Un matís que sí que és útil, en canvi: memo pot impedir que el component arribi a tornar-se a executar per causa del pare, i en aquest cas el context ni tan sols es consulta. És a dir, memo no bloqueja la propagació del context, però sí que pot reduir quantes vegades s'executa el component per altres causes.

  1. Quan aplicar-lo i quan no

Aplica memo quan es compleixin les tres condicions alhora:

  1. El component es torna a executar sovint amb les mateixes props. Ho has comprovat, no ho suposes.
  2. Executar-lo és car: subarbre gran, molts elements, càlculs, formateig amb Intl, o està repetit centenars de vegades en una llista.
  3. Les seves props són estables o pots fer que ho siguin amb useMemo/useCallback.

Els casos de CicloUrbano que compleixen les tres:

Component Per què compensa
TargetaBicicleta 2.000 instàncies; el pare es repinta amb cada tecla del cercador
ResumFlota Recorre les 2.000 bicicletes per agregar per estat; les seves props canvien poques vegades
PanellReserves Subarbre gran dins d'una pàgina que es repinta per altres motius
LlistaAvisos Penja del Disseny, que es torna a executar amb tota navegació

No apliquis memo quan:

Situació Per què no
El component és barat (EtiquetaEstat, IndicadorDeCarrega, MollesDePa) Comparar costa més que executar-lo
Les seves props gairebé sempre canvien La comparació falla sempre: cost pur
Rep children que es creen al pare La prop children invalida la comparació gairebé sempre
Ja està aïllat per estructura (rep children, o el seu pare no es repinta) El problema ja no existeix
És una pàgina (PaginaCataleg, PaginaReserves) Es renderitza quan canvia la ruta, és a dir, quan ha de fer-ho
Es renderitza una sola vegada per pantalla i sense repeticions No hi ha res a estalviar
Vols memoïtzar «per si de cas», sense mesurar És la definició d'optimització prematura

  1. El cost de memo

memo no és gratuït, i el seu cost té tres components:

  1. Comparació a cada render del pare. Recórrer les claus de les props i cridar Object.is per cadascuna. És barat, però multiplicat per 2.000 instàncies i per cada pulsació de tecla deixa de ser-ho.
  2. Memòria. React conserva les props anteriors i l'arbre d'elements produït. Amb 2.000 targetes memoïtzades, això és memòria real que no s'allibera mentre el component estigui muntat.
  3. Cost humà. Cada memo és un contracte implícit: «les props d'aquest component s'han de mantenir estables». Qui afegeixi demà un style={{...}} en línia trencarà aquest contracte sense adonar-se'n, i el memo quedarà com a cost sense benefici, invisible per sempre.

D'aquí la conclusió que més codi estalvia de tota la lliçó:

Omplir el projecte de memo produeix una aplicació més lenta, no més ràpida. Cada memo que no encerta és comparació i memòria a canvi de res, i els que no encerten són la majoria si ningú ha mesurat.

Un experiment mental que ho aclareix: si embolcalles els 26 components de CicloUrbano en memo, afegeixes 26 comparacions per render i unes quantes desenes de kilobytes de props guardades. D'aquests 26, potser 4 estan en una llista llarga o protegeixen un subarbre car. Els altres 22 són cost pur. I cap dels 4 funcionarà si les seves props no són estables.

  1. memo i el React Compiler

Com es va avançar a 08-01, el React Compiler de React 19 aplica aquesta mateixa memoïtzació de manera automàtica. En un projecte compilat, TargetaBicicleta es salta el render quan les seves props no han canviat sense que escriguis memo, i —això és el veritablement rellevant— el compilador també estabilitza les funcions fletxa que LlistaBicicletes crea per a cada targeta, amb la qual cosa la fallada de l'apartat 5 no arriba a produir-se.

Què significa això a la pràctica:

Amb el compilador activat Situació
memo explícit en un component ja optimitzat pel compilador Redundant, però inofensiu: no trenca res
Components que el compilador omet (trenquen les regles de React) El memo manual continua sent l'única protecció
Comparadors personalitzats No els substitueix: expressen una decisió que el compilador no pot inferir
Renders per estat propi o per context Igual que sense compilador: no els evita
Projectes sense el compilador (la gran majoria avui) Tot el d'aquesta lliçó continua sent necessari tal qual

I la raó de fons per la qual aquesta lliçó no sobra: quan alguna cosa va malament —una targeta que no s'actualitza, un render que no se salta— el diagnòstic consisteix exactament a preguntar-se quina prop ha canviat d'identitat i per què. Aquest raonament és el mateix amb compilador i sense ell. El compilador t'estalvia escriure memo; no t'estalvia entendre'l.

Errors Comuns i Consells

Embolcallar en memo i deixar les props en línia. És l'error número u, i produeix el pitjor dels dos mons: el mateix nombre de renders més una comparació per cadascun. Si vols memoïtzar, estabilitza les props (08-03) o no memoïtzis.

Creure que memo evita el render per estat o per context. No ho fa mai. Un memo en un component que consumeix useTema o useSelector és decoració.

Invertir el comparador personalitzat. Retornar true per «ha canviat» produeix un component congelat. Recorda: memo pregunta «són iguals?».

memo(() => …) amb funció anònima. El Profiler i les eines de desenvolupament mostraran Anonymous, i perdràs la meitat de la utilitat de 08-05. Anomena sempre la funció.

Memoïtzar un component que rep children. children és una prop, i en el 95 % dels casos és un element nou a cada render del pare. Si el teu component contenidor rep children, ja està aïllat per estructura (08-01, tècnica 2) i no necessita memo.

Consell: memo és l'últim recurs, no el primer. Abans: puc baixar l'estat? puc passar children? puc paginar? puc passar primitius en lloc de l'objecte sencer? Si alguna resposta és sí, aquesta és la solució.

Consell: passa primitius sempre que puguis. <TargetaBicicleta model={b.model} estat={b.estat} preuHora={b.preuHora} /> memoïtza bé sense cap esforç, perquè els primitius es comparen per valor. És més verbós i molt més robust.

Consell: memoïtza el contenidor, no les fulles. Un memo a LlistaBicicletes protegeix un subarbre de 2.000 elements amb una sola comparació. Un memo a cada EtiquetaEstat afegeix 2.000 comparacions per estalviar 2.000 <span>. Si pots tallar a dalt, talla a dalt.

Exercicis

Exercici 1. Per a cadascun d'aquests quatre usos de memo a CicloUrbano, indica si el memo funciona, no funciona o és innecessari, i justifica la resposta en una frase.

// a)
const EtiquetaEstat = memo(function EtiquetaEstat({ estat }) {
  return <span className={estils[estat]}>{ETIQUETES[estat]}</span>;
});

// b)
const ResumFlota = memo(function ResumFlota({ bicicletes }) {
  const perEstat = bicicletes.reduce((acc, b) => { /* … */ }, {});
  return <dl>{/* … */}</dl>;
});
// Ús: <ResumFlota bicicletes={data.filter((b) => b.estacioId === estacioId)} />

// c)
const BotoTema = memo(function BotoTema() {
  const { tema, alternarTema } = useTema();
  return <button onClick={alternarTema}>{tema}</button>;
});

// d)
const Panell = memo(function Panell({ titol, children }) {
  return <section><h2>{titol}</h2>{children}</section>;
});

Exercici 2. PanellReserva està embolcallat en memo i tot i així es torna a executar a cada render del seu pare. Troba les tres props que ho impedeixen i explica quin tipus de valor és cadascuna. No cal que les arreglis: això és 08-03.

function PaginaFitxaBicicleta() {
  const { bicicletaId } = useParams();
  const { data: bicicleta } = useBicicleta(bicicletaId);
  const [hores, setHores] = useState(1);

  return (
    <PanellReserva
      bicicleta={bicicleta}
      hores={hores}
      tarifes={{ hora: bicicleta.preuHora, diposit: 20 }}
      tipusPermesos={['urbana', 'electrica']}
      alConfirmar={(dades) => crearReserva(dades)}
      estil={{ marginTop: 16 }}
    />
  );
}

Exercici 3. TargetaBicicleta rep l'objecte bicicleta complet, però només usa model, estat, preuHora i id. L'objecte ve de la caché de TanStack Query i se substitueix sencer a cada revalidació (cada 30 segons pel staleTime del catàleg), encara que les dades siguin idèntiques. Proposa dues solucions diferents —una amb comparador personalitzat i una altra sense memo en absolut— i argumenta quina triaries.

Solucions

Solució 1.

Cas Veredicte Justificació
a) EtiquetaEstat Innecessari Rep un primitiu, així que la comparació encerta, però el component és un <span>: comparar costa el mateix o més que executar-lo. Cost sense benefici
b) ResumFlota No funciona La prop bicicletes és el resultat d'un .filter() al punt d'ús: array nou a cada render. La comparació falla sempre, i a més el component és car. És el pitjor escenari possible
c) BotoTema Innecessari No rep props, així que memo sempre diu «iguals», però es torna a executar igualment cada vegada que canvia el context de tema. memo no bloqueja el context
d) Panell No funciona children és una prop i el seu element es crea de nou a cada render del pare. Un contenidor amb children ja està aïllat per estructura i no necessita memo

Solució 2. Tres props trenquen la comparació:

  1. tarifes={{ hora: ..., diposit: 20 }}objecte literal: nou a cada render encara que preuHora no canviï.
  2. tipusPermesos={['urbana', 'electrica']}array literal: nou a cada render, amb contingut constant. És el cas més absurd dels tres, perquè el valor no depèn de res i podria viure fora del component.
  3. alConfirmar={(dades) => crearReserva(dades)}funció fletxa en línia: nova a cada render.
  4. I una quarta de regal: estil={{ marginTop: 16 }} — objecte literal, el cas més freqüent de tots.

bicicleta sí que és estable (ve de la caché de Query) i hores és un número, comparat per valor. Amb quatre props trencades de sis, memo no se salta ni un sol render: només afegeix la comparació.

Solució 3.

Opció A, amb comparador personalitzat:

function sonEquivalents(previes, noves) {
  return (
    previes.bicicleta.id === noves.bicicleta.id &&
    previes.bicicleta.model === noves.bicicleta.model &&
    previes.bicicleta.estat === noves.bicicleta.estat &&
    previes.bicicleta.preuHora === noves.bicicleta.preuHora &&
    previes.nomEstacio === noves.nomEstacio &&
    previes.alSeleccionar === noves.alSeleccionar &&
    previes.alReservar === noves.alReservar
  );
}

export default memo(TargetaBicicleta, sonEquivalents);

Funciona, però crea un deute de manteniment: el dia que la targeta mostri bicicleta.tipus, aquest camp deixarà d'actualitzar-se en pantalla i ningú rebrà cap avís.

Opció B, sense memo: passar primitius.

<TargetaBicicleta
  id={bicicleta.id}
  model={bicicleta.model}
  estat={bicicleta.estat}
  preuHora={bicicleta.preuHora}
  nomEstacio={nomEstacio}
  alSeleccionar={alSeleccionar}
  alReservar={alReservar}
/>

Amb props primitives, la comparació superficial per defecte de memo encerta sempre, sense comparador i sense deute: si l'objecte se substitueix però model, estat i preuHora valen el mateix, Object.is dona true en les tres. A més, el contracte del component queda explícit: es veu d'un cop d'ull què necessita.

Quina triar: l'opció B. És més robusta, s'autodocumenta i no es pot quedar desactualitzada. L'únic argument a favor d'A és la comoditat de passar un objecte, i aquesta comoditat es paga amb una fallada silenciosa. La regla general: quan un comparador personalitzat sembla necessari, revisa primer el contracte de props.

Conclusió

React.memo fa una sola cosa i la fa bé: embolcalla un component i, abans d'executar-lo, compara les seves props per identitat i de manera superficial amb les del render anterior; si són totes iguals, se salta l'execució i reutilitza l'arbre d'elements que va produir l'última vegada, tallant a més tot el subarbre que en penjava. Aplicat a TargetaBicicleta dins d'un catàleg de 2.000 elements, el potencial és enorme —sobretot després de treure l'Intl.NumberFormat fora del component, que és l'optimització que funciona amb memo i sense ell.

Però el potencial no es materialitza sol. memo compara per identitat, així que basta una funció fletxa en línia, un objecte literal, un style={{}}, un array construït al vol o un children perquè la comparació falli sempre i el memo es converteixi en cost pur. És exactament el que passa avui a LlistaBicicletes amb alSeleccionar i alReservar, i ja saps diagnosticar-ho amb console.count i amb un comparador de rastreig que assenyala la prop culpable. La meitat que falta —useMemo i useCallback per estabilitzar aquestes identitats— és la lliçó següent, i fins llavors el memo del catàleg no estalvia ni un sol render.

També has vist els límits. El comparador personalitzat (propsPrevies, propsNoves) => boolean té la signatura invertida respecte a shouldComponentUpdate: aquí true significa «són iguals, no renderitzis», i confondre-ho produeix components congelats sense cap missatge d'error; comparar en profunditat gairebé sempre surt més car que renderitzar, i quan un comparador sembla necessari, el que sol fallar és el contracte de props. I sobretot: memo intervé en una sola de les quatre causes de render. No bloqueja mai el render per estat propi, ni per context consumit, ni per un magatzem extern subscrit. Un memo sobre BotoTema és decoració.

D'aquí el criteri: memoïtza quan es compleixin les tres condicions —es torna a executar sovint amb les mateixes props, executar-lo és car, i les seves props són o es poden fer estables— i no memoïtzis components barats, props volàtils, contenidors amb children ni subarbres ja aïllats per estructura. Cada memo costa una comparació per render, memòria i un contracte implícit que el proper desenvolupador trencarà sense adonar-se'n: omplir el projecte de memo produeix una aplicació més lenta. El React Compiler automatitza aquesta memoïtzació quan està activat, però no substitueix els comparadors personalitzats, no evita els renders per estat o context, omet el codi que trenca les regles de React, i no t'estalvia el raonament —quina prop ha canviat d'identitat i per què— que és justament el que cal quan alguna cosa va malament.

Queda pendent el deute més citat del mòdul: com aconseguir que alSeleccionar sigui la mateixa funció entre renders, que tarifes sigui el mateix objecte, que el value d'un proveïdor de context no canviï d'identitat sense motiu (el deute obert a 07-02) i que ordenar 2.000 bicicletes no es repeteixi quan res ha canviat. Tot això són dos hooks. La propera lliçó és Hooks useMemo i useCallback.

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