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
- Què significa memoïtzar
React.memo: signatura i comportament exacte- L'arbre de render amb i sense
memo - Cas pràctic:
TargetaBicicletadins deLlistaBicicletes - Per què
memosovint no serveix de res - Diagnòstic: reproduir la fallada i veure-la
- El comparador personalitzat
- Signatura invertida respecte a
shouldComponentUpdate - Què bloqueja
memoi què no bloqueja mai - Quan aplicar-lo i quan no
- El cost de
memo memoi el React Compiler
- 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 calcularD'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 unMap: 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.
React.memo: signatura i comportament exacte
React.memo: signatura i comportament exactememo 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:
- Recupera les props del render anterior.
- Compara els dos objectes de props superficialment (shallow): recorre les claus de primer nivell i compara cada valor amb
Object.is. - Si totes són iguals, no crida la funció del component i reutilitza l'arbre d'elements que va produir l'última vegada.
- 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 renderitzaPer 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.
- L'arbre de render amb i sense
memo
memoflowchart 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.
- Cas pràctic:
TargetaBicicleta dins de LlistaBicicletes
TargetaBicicleta dins de LlistaBicicletesAquest é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:
- Treure
Intl.NumberFormatfora del component. És l'optimització real: elimina 2.000 construccions d'objecte per render sense memoïtzar res, i continua funcionant encara que elmemono encerti mai. És la tècnica 3 de 08-01 —evitar el treball, no accelerar-lo— aplicada a una constant. - Embolcallar l'exportació en
memo. La convenció del projecte («export defaultper 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 escriusexport default memo(({ bicicleta }) => …), React DevTools mostrarà el component comAnonymousi 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è.
- Per què
memo sovint no serveix de res
memo sovint no serveix de resAquest é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 igualmentBasta 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:
memonomé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.
- 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.
- El comparador personalitzat
memo accepta un segon argument: una funció que decideix la comparació en el teu lloc.
El contracte és precís:
- Retorna
truesi consideres que les props són iguals → React no renderitza. - Retorna
falsesi 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.barrii 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. childrengairebé sempre l'invalida. Si el component repchildren, 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 iundefined.
// ❌ 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,barriiplacesLliurescom a primitius en lloc de l'objecte sencer elimina el problema sense comparador i sensememo.
- Signatura invertida respecte a
shouldComponentUpdate
shouldComponentUpdateA 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.
- Què bloqueja
memo i què no bloqueja mai
memo i què no bloqueja maiAquest é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í | É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 sí 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.
- Quan aplicar-lo i quan no
Aplica memo quan es compleixin les tres condicions alhora:
- El component es torna a executar sovint amb les mateixes props. Ho has comprovat, no ho suposes.
- Executar-lo és car: subarbre gran, molts elements, càlculs, formateig amb
Intl, o està repetit centenars de vegades en una llista. - 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 |
- El cost de
memo
memomemo no és gratuït, i el seu cost té tres components:
- Comparació a cada render del pare. Recórrer les claus de les props i cridar
Object.isper cadascuna. És barat, però multiplicat per 2.000 instàncies i per cada pulsació de tecla deixa de ser-ho. - 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.
- Cost humà. Cada
memoés un contracte implícit: «les props d'aquest component s'han de mantenir estables». Qui afegeixi demà unstyle={{...}}en línia trencarà aquest contracte sense adonar-se'n, i elmemoquedarà 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
memoprodueix una aplicació més lenta, no més ràpida. Cadamemoque 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.
memo i el React Compiler
memo i el React CompilerCom 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ó:
tarifes={{ hora: ..., diposit: 20 }}— objecte literal: nou a cada render encara quepreuHorano canviï.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.alConfirmar={(dades) => crearReserva(dades)}— funció fletxa en línia: nova a cada render.- 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
- 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
