La lliçó anterior va acabar amb un memo sobre TargetaBicicleta que no estalvia ni un sol render, perquè LlistaBicicletes crea funcions fletxa noves a cada execució i la comparació per identitat falla sempre. Aquest és el problema que aquests dos hooks resolen, i no és l'únic: el mòdul 7 va deixar oberta la mateixa deuda en tres llocs més —el value dels proveïdors de context (07-02), les instàncies de selectors de Redux (07-05) i les dependències dels efectes (05-02)—, i tots són la mateixa qüestió: com aconseguir que un valor construït dins d'un component continuï sent el mateix objecte entre renders. A això s'hi suma un problema diferent que també resol useMemo: evitar que un càlcul realment car —ordenar i filtrar 2.000 bicicletes— es repeteixi quan les seves entrades no han canviat. Distingir aquests dos usos és el més important d'aquesta lliçó, perquè gairebé tot el mal ús de la memoïtzació ve de confondre'ls. Al final veuràs a més els hooks de concurrència de React 18/19, useTransition i useDeferredValue, que ataquen el mateix símptoma des d'un angle completament diferent: en lloc de fer menys feina, reorganitzar quan es fa.

Contingut

  1. useMemo: signatura i què guarda exactament
  2. useCallback: el cas particular de useMemo
  3. Els dos hooks, comparats
  4. Ús legítim 1: evitar un càlcul realment costós
  5. Com comprovar de veritat si un càlcul és car
  6. Ús legítim 2: mantenir la identitat d'un valor
  7. Tancant 08-02: LlistaBicicletes definitiva
  8. Tancant 07-02: el value d'un proveïdor de context
  9. Dependències de useEffect i selectors de Redux
  10. Com triar les dependències i per què el linter té raó
  11. Quan una dependència canvia massa
  12. Quan NO memoïtzar
  13. Alternatives millors que memoïtzar
  14. Hooks de concurrència: useTransition i useDeferredValue
  15. El React Compiler aquí

  1. useMemo: signatura i què guarda exactament

const valorMemoitzat = useMemo(() => calcularAlgo(a, b), [a, b]);

Tres parts:

  • La fàbrica: una funció sense arguments que retorna el valor. React la crida; tu no.
  • L'array de dependències: els valors dels quals depèn el resultat.
  • El valor retornat: el que va produir la fàbrica.

I el comportament, amb precisió:

  1. Al primer render, React executa la fàbrica i guarda el resultat junt amb les dependències.
  2. Als següents, compara cada dependència amb la guardada usant Object.is.
  3. Si totes coincideixen, retorna el valor guardat sense executar la fàbrica.
  4. Si alguna difereix, executa la fàbrica una altra vegada, guarda el nou resultat i les noves dependències.

Dos advertències que canvien com s'escriu el codi:

La fàbrica s'executa durant el render, així que ha de ser pura: res de peticions, temporitzadors, escriptures al DOM ni setEstat. Per a això hi ha els efectes (05-02).

useMemo és una optimització, no una garantia. La documentació de React ho diu explícitament: React pot descartar valors memoïtzats quan ho necessiti (per exemple, per alliberar memòria de components fora de pantalla). No escriguis mai codi la correcció del qual depengui que la fàbrica no es torni a executar. Si necessites que alguna cosa passi una sola vegada de debò, això és un useRef o un efecte, no un useMemo.

  1. useCallback: el cas particular de useMemo

const gestionarReserva = useCallback((id) => { /* … */ }, [usuari]);

useCallback guarda la funció mateixa, no el resultat de cridar-la. I aquesta equivalència ho explica tot:

// Aquestes dues línies fan exactament el mateix
const f = useCallback((id) => reservar(id), [reservar]);
const f = useMemo(() => (id) => reservar(id), [reservar]);

useCallback(fn, deps) és useMemo(() => fn, deps). Existeix com a hook propi només perquè memoïtzar funcions és tan freqüent que la doble fletxa resultava incòmoda i propensa a errors.

L'error de principiant que cal evitar des del primer dia:

// ❌ Crida la funció i memoïtza el seu RESULTAT
const total = useCallback(calcularTotal(hores), [hores]);

// ✅ Memoïtza la FUNCIÓ
const calcular = useCallback(() => calcularTotal(hores), [hores]);

// ✅ Memoïtza el RESULTAT — que és el que probablement volies
const total = useMemo(() => calcularTotal(hores), [hores]);

  1. Els dos hooks, comparats

useMemo useCallback
Què guarda El valor retornat per la fàbrica La funció que li passes
Signatura useMemo(() => valor, deps) useCallback(fn, deps)
Quan recalcula Quan canvia alguna dependència Quan canvia alguna dependència
Equivalència useMemo(() => fn, deps)
Motiu 1: cost ✅ La seva raó principal ❌ Crear una funció és baratíssim
Motiu 2: identitat ✅ Objectes, arrays, instàncies ✅ La seva raó única
Ús típic Filtrar/ordenar llistes, value de context, objectes d'opcions Gestors passats a components memoïtzats o a dependències d'efectes
La fàbrica s'executa Durant el render Mai l'executa React: només la guarda

Un matís que ordena la taula: useCallback mai no és una optimització de cost. Crear una funció en JavaScript no costa pràcticament res. useCallback existeix només per preservar la identitat. Si ningú compara aquesta funció —no va a un component memoïtzat, no és dependència d'un efecte ni d'un altre hook—, el useCallback és cost pur.

  1. Ús legítim 1: evitar un càlcul realment costós

Aquest és el catàleg de CicloUrbano amb les 2.000 bicicletes, filtrant i ordenant al render:

// src/pagines/PaginaCataleg.jsx (sense memoïtzar)
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router';
import { useBicicletes, useEstacions } from '../consultes/consultesBicicletes.js';

function PaginaCataleg() {
  const { data: bicicletes = [] } = useBicicletes();
  const { data: estacions = [] } = useEstacions();
  const terme = useSelector((estat) => estat.cataleg.terme);
  const ordre = useSelector((estat) => estat.cataleg.ordre);
  const [parametres] = useSearchParams();
  const tipus = parametres.get('tipo') ?? 'todos';

  // Aquest bloc s'executa a CADA render, canviï el que canviï
  const visibles = bicicletes
    .filter((b) => tipus === 'todos' || b.tipus === tipus)
    .filter((b) => b.model.toLowerCase().includes(terme.toLowerCase()))
    .map((b) => ({
      ...b,
      nomEstacio: estacions.find((e) => e.id === b.estacioId)?.nom ?? '—'
    }))
    .sort((a, b) =>
      ordre === 'preu' ? a.preuHora - b.preuHora : a.model.localeCompare(b.model)
    );

  return (
    <>
      <CercadorBicicletes />
      <SelectorTipus />
      <LlistaBicicletes bicicletes={visibles} />
    </>
  );
}

Dos problemes independents, i convé no barrejar-los:

  • Cost: amb 2.000 bicicletes hi ha dos filtratges, un map que fa un find sobre les estacions per cada element —això és O(n × m)— i una ordenació amb localeCompare, que és de les comparacions més cares de JavaScript. Entre 10 i 40 ms per render en un equip de desenvolupament, i tres o quatre vegades més en un mòbil.
  • Identitat: visibles és un array nou a cada render, així que qualsevol memo sobre LlistaBicicletes estaria condemnat a fallar.

useMemo resol els dos alhora:

const visibles = useMemo(() => {
  // 1) Índex d'estacions: converteix el find O(m) en un accés O(1)
  const nomPerEstacio = new Map(estacions.map((e) => [e.id, e.nom]));
  // 2) Collator reutilitzable: crear un Intl.Collator per comparació és carissim
  const collator = new Intl.Collator('ca');
  const termeNormalitzat = terme.trim().toLowerCase();

  return bicicletes
    .filter((b) => tipus === 'todos' || b.tipus === tipus)
    .filter((b) => b.model.toLowerCase().includes(termeNormalitzat))
    .map((b) => ({ ...b, nomEstacio: nomPerEstacio.get(b.estacioId) ?? '—' }))
    .sort((a, b) =>
      ordre === 'preu' ? a.preuHora - b.preuHora : collator.compare(a.model, b.model)
    );
}, [bicicletes, estacions, terme, tipus, ordre]);

Fixa't en el que ha passat aquí, perquè és la lliçó de fons: abans de memoïtzar, s'ha reduït la feina. El Map d'estacions converteix una cerca lineal per element en un accés directe, i l'Intl.Collator reutilitzat evita construir un comparador a cada crida de sort. Aquestes dues línies baixen el cost més que el useMemo, i continuen servint encara que les dependències canviïn a cada render. La memoïtzació arriba després, no en lloc de.

Amb el useMemo posat, aquesta és la taula de comportament:

Què canvia Recalcula? Correcte?
L'usuari escriu al cercador Sí (terme) Sí: el resultat en depèn
Canvia ?tipo= a la URL Sí (tipus)
Canvia l'ordre Sí (ordre)
Query revalida i retorna les mateixes dades Sí (bicicletes és un array nou) Inevitable: Query substitueix la referència
S'obre el Modal (estat local de la pàgina) No Aquest és el guany
Canvia el tema No Idem
Arriba un avís nou No Idem

  1. Com comprovar de veritat si un càlcul és car

«Car» no és una opinió. Es mesura, i hi ha dues maneres ràpides abans d'obrir el Profiler.

Amb console.time, embolcallant el càlcul sense memoïtzar:

const visibles = useMemo(() => {
  console.time('filtrar i ordenar cataleg');
  const resultat = /* … el càlcul … */;
  console.timeEnd('filtrar i ordenar cataleg');
  return resultat;
}, [bicicletes, estacions, terme, tipus, ordre]);

Com interpretar el número que surti per consola:

Durada mesurada Veredicte
< 1 ms No memoïtzis. El useMemo costa més que el càlcul
1 – 5 ms Zona grisa: depèn de amb quina freqüència passi i de si hi ha més feina al mateix render
5 – 16 ms Memoïtzar probablement compensa: t'acostes al pressupost d'un fotograma
> 16 ms Memoïtza, i a més revisa l'algorisme: probablement hi ha un find dins d'un map

La referència dels 16 ms ve de que a 60 fotogrames per segon el navegador disposa de 16,7 ms per fotograma. Tot el que se'n passi produeix un salt visible.

Amb la ralentització de CPU de les eines del navegador. A la pestanya Rendiment (Performance) hi ha un selector de CPU throttling: posa'l en o . És el més semblant a un mòbil de gamma mitjana que tens sense comprar-ne un. Un càlcul de 4 ms al teu portàtil passa a 16–24 ms allà, i el que era irrellevant deixa de ser-ho.

I el pas que gairebé ningú fa: treu el useMemo i mesura una altra vegada. Si el número no canvia de forma perceptible, treu-lo definitivament. És el pas «tornar a mesurar» del flux de treball de 08-01, i és el que distingeix optimitzar de decorar.

  1. Ús legítim 2: mantenir la identitat d'un valor

El segon ús no té res a veure amb el cost del càlcul. Aquí la fàbrica pot ser trivial —() => ({ usuari, esOperari })— i tot i així el useMemo és imprescindible, perquè el que importa no és el que costa construir el valor, sinó que algú més avall el compararà per identitat.

Qui compara? Exactament aquests quatre:

Qui compara Què passa si la identitat canvia
memo en un component fill La comparació falla i el component es torna a executar sempre (08-02)
useEffect amb aquesta dependència L'efecte es torna a executar: petició repetida, temporitzador reiniciat, subscripció refeta
Un proveïdor de context (value) Tots els consumidors es tornen a executar, usin o no la part que ha canviat (07-02)
useSelector de Redux o un selector memoïtzat Repintat a cada acció despatxada, o memoïtzació que mai encerta (07-05)

I n'hi ha un cinquè, silenciós: un altre useMemo o useCallback que tingui aquest valor entre les seves dependències. Una identitat inestable es propaga en cascada i invalida tota la memoïtzació que hi ha per sota. Per això els problemes d'identitat s'arreglen des de dalt: estabilitza primer el valor de més amunt i molts dels de sota s'arreglen sols.

  1. Tancant 08-02: LlistaBicicletes definitiva

Aquest és el codi que 08-02 va deixar pendent.

// src/components/LlistaBicicletes.jsx (versió definitiva)
import { useCallback } from 'react';
import { useNavigate } from 'react-router';
import TargetaBicicleta from './TargetaBicicleta.jsx';
import estils from './LlistaBicicletes.module.css';

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

  // navegar és estable: React Router en garanteix la identitat entre renders.
  // Amb [navegar] com a dependència, aquestes funcions es creen UNA vegada.
  const gestionarSeleccio = useCallback(
    (id) => navegar(`/bicicletas/${id}`),
    [navegar]
  );

  const gestionarReserva = useCallback(
    (id) => navegar(`/reservas/nueva?bicicleta=${id}`),
    [navegar]
  );

  return (
    <ul className={estils.reixeta}>
      {bicicletes.map((bicicleta) => (
        <li key={bicicleta.id}>
          <TargetaBicicleta
            bicicleta={bicicleta}
            nomEstacio={bicicleta.nomEstacio}
            alSeleccionar={gestionarSeleccio}
            alReservar={gestionarReserva}
          />
        </li>
      ))}
    </ul>
  );
}

export default LlistaBicicletes;

Tres decisions i els seus motius:

  1. useCallback amb [navegar]. La funció navegar de React Router és estable per disseny, així que les dues funcions es creen una sola vegada en tota la vida del component. Ara Object.is(propsPrevies.alSeleccionar, propsNoves.alSeleccionar) dona true i el memo de 08-02 per fi funciona.
  2. nomEstacio ve ja calculat des del useMemo de PaginaCataleg, en lloc de resoldre's aquí amb un find per cada targeta. Menys feina i una prop primitiva més.
  3. Els gestors reben l'id com a argument, no es crea una funció per targeta. Si TargetaBicicleta necessités () => alReservar(bicicleta.id), aquesta fletxa es crearia dins de la targeta, que és on ha d'estar: és una funció que la targeta passa a un element del DOM, i a un onClick d'un <button> li és igual la identitat.

Aquest és l'escenari on memo i useCallback funcionen: junts, en una llista llarga, amb una mesura prèvia que ho justifica. Cap dels dos serveix de res per separat.

  1. Tancant 07-02: el value d'un proveïdor de context

La deuda que el mòdul 7 va deixar explícitament pendent. El problema, recordat en una línia: el value d'un proveïdor és un objecte literal creat a cada render, així que cada render del proveïdor torna a executar tots els seus consumidors, encara que el contingut sigui idèntic.

// ❌ El problema
export function ProveidorTema({ children }) {
  const [tema, setTema] = useMagatzemLocal('tema', 'clar');

  // Objecte NOU a cada render → tots els consumidors es tornen a executar
  const valor = { tema, alternarTema: () => setTema((t) => (t === 'clar' ? 'fosc' : 'clar')) };

  return <ContextTema value={valor}>{children}</ContextTema>;
}
// ✅ La versió definitiva de src/contextos/ContextTema.jsx
import { createContext, useContext, useMemo, useCallback, useEffect } from 'react';
import { useMagatzemLocal } from '../hooks/useMagatzemLocal.js';

const ContextTema = createContext(null);

export function ProveidorTema({ children }) {
  const [tema, setTema] = useMagatzemLocal('tema', 'clar');

  useEffect(() => {
    document.documentElement.dataset.tema = tema;
  }, [tema]);

  // 1) La funció, estable: setTema de useState és estable per contracte de React
  const alternarTema = useCallback(() => {
    setTema((actual) => (actual === 'clar' ? 'fosc' : 'clar'));
  }, [setTema]);

  // 2) L'objecte, estable mentre no canviï el tema
  const valor = useMemo(() => ({ tema, alternarTema }), [tema, alternarTema]);

  return <ContextTema value={valor}>{children}</ContextTema>;
}

export function useTema() {
  const context = useContext(ContextTema);
  if (context === null) {
    throw new Error('useTema s\'ha d\'usar dins de <ProveidorTema>');
  }
  return context;
}

Què aconsegueix i què no, amb precisió:

  • Aconsegueix: si ProveidorTema es torna a executar per un motiu aliè al tema —per exemple, perquè el seu pare s'ha repintat—, valor és el mateix objecte, Object.is dona true i React no notifica cap consumidor.
  • No aconsegueix: quan el tema canvia, tots els consumidors es tornen a executar, inclosos els que només usen alternarTema. Per a això hi ha la divisió de contextos en estat i accions de 07-02, que és una solució estructural i complementària.

Aplicat a ContextAvisos, on la divisió ja existeix, el resultat és el patró complet:

// src/contextos/ContextAvisos.jsx (fragment amb la memoïtzació completa)
export function ProveidorAvisos({ children }) {
  const [avisos, setAvisos] = useState([]);

  const descartarAvis = useCallback((id) => {
    setAvisos((previs) => previs.filter((avis) => avis.id !== id));
  }, []);

  const mostrarAvis = useCallback((to, text, duracio = 5000) => {
    const id = `avis-${crypto.randomUUID().slice(0, 8)}`;
    setAvisos((previs) => [...previs, { id, to, text }]);
    if (duracio > 0) setTimeout(() => descartarAvis(id), duracio);
    return id;
  }, [descartarAvis]);

  // Les ACCIONS no canvien MAI: els seus consumidors no es tornen a executar mai
  const accions = useMemo(
    () => ({ mostrarAvis, descartarAvis }),
    [mostrarAvis, descartarAvis]
  );

  return (
    <ContextAvisosAccions value={accions}>
      <ContextAvisosEstat value={avisos}>
        {children}
      </ContextAvisosEstat>
    </ContextAvisosAccions>
  );
}

El resultat combinat és exactament el que es buscava a 07-02: un component com FormulariReserva, que només crida mostrarAvis i mai llegeix la llista, no es torna a executar mai per culpa dels avisos. Abans es tornava a executar amb cada avís que apareixia o desapareixia en qualsevol punt de l'aplicació.

Regla operativa, ara justificada: tot proveïdor que publiqui un objecte literal com a value ha d'estabilitzar-lo amb useMemo, i les funcions que contingui amb useCallback. És dels poquíssims llocs on memoïtzar és l'opció per defecte i no una optimització prematura, perquè el cost de no fer-ho es propaga a tota l'aplicació.

  1. Dependències de useEffect i selectors de Redux

En efectes (represa de 05-02): una funció o un objecte a l'array de dependències d'un useEffect provoca que l'efecte es torni a executar a cada render.

// ❌ Bucle de peticions: opcions és un objecte nou cada render
function PanellIncidencies({ estacioId }) {
  const opcions = { estacioId, incloureTancades: false };

  useEffect(() => {
    carregarIncidencies(opcions).then(setIncidencies);
  }, [opcions]);              // canvia SEMPRE → petició a cada render
}

// ✅ Opció A: estabilitzar l'objecte
const opcions = useMemo(() => ({ estacioId, incloureTancades: false }), [estacioId]);

// ✅ Opció B, millor: no crear l'objecte fora de l'efecte
useEffect(() => {
  carregarIncidencies({ estacioId, incloureTancades: false }).then(setIncidencies);
}, [estacioId]);              // només primitius

L'opció B és gairebé sempre preferible, i aquest és l'ensenyament: quan una dependència inestable causa problemes en un efecte, la primera pregunta no és «com la memoïtzo?» sinó «per què és fora de l'efecte?».

En selectors de Redux (represa de 07-05): un selector creat en línia que construeix un valor retorna una referència nova a cada crida, i useSelector compara per identitat, així que el component es repinta amb cada acció despatxada a l'aplicació.

// ❌ Objecte nou a cada execució del selector
const { actives, total } = useSelector((estat) => ({
  actives: estat.reserves.ids.filter((id) => estat.reserves.entitats[id].estat === 'activa'),
  total: estat.reserves.ids.length
}));

// ✅ Selector memoïtzat al slice (createSelector), llegit sense construir res
const resum = useSelector(seleccionarResumReserves);

// ✅ Fàbrica de selectors amb argument, amb la seva instància estabilitzada
const selectorReserves = useMemo(crearSelectorReservesUsuari, []);
const reserves = useSelector((estat) => selectorReserves(estat, usuariId));

La distinció entre les tres capes de memoïtzació que ara conviuen a CicloUrbano:

Capa Eina Àmbit Què evita
Magatzem createSelector Global, compartit per tots els components Recalcular derivacions de l'estat de Redux
Component useMemo Una instància d'un component Recalcular a cada render d'aquest component
Servidor staleTime de Query Global, per clau de consulta Tornar a demanar dades al servidor

  1. Com triar les dependències i per què el linter té raó

La regla és exacta i no admet excepcions: l'array de dependències ha de contenir tots els valors reactius que la fàbrica llegeix. Valor reactiu és qualsevol cosa declarada dins del component que pugui canviar entre renders: props, estat, valors de context, i qualsevol variable derivada d'ells.

function PanellReserva({ bicicleta, descompte }) {
  const [hores, setHores] = useState(1);
  const { usuari } = useSessio();

  const preuTotal = useMemo(
    () => bicicleta.preuHora * hores * (1 - descompte),
    [bicicleta.preuHora, hores, descompte]   // els tres valors que llegeix, cap més
  );
  // `usuari` NO és una dependència: la fàbrica no la llegeix
}

Què passa si t'equivoques, en cada direcció:

Error Conseqüència Gravetat
Falta una dependència El valor es queda congelat amb dades velles. La pantalla menteix Greu: és una fallada de correcció
Sobra una dependència Recalcula de més. Només es perd l'optimització Lleu
Array buit [] en un valor que depèn de props Congelat per sempre Molt greu
Sense array (ometre'l) Recalcula sempre: el useMemo no fa res Lleu, però és codi mort

Per això eslint-plugin-react-hooks amb la regla exhaustive-deps no és opcional en un projecte seriós, i per això silenciar-la amb un comentari gairebé sempre amaga un problema de disseny:

// ❌ El senyal d'alarma més fiable de l'ecosistema React
// eslint-disable-next-line react-hooks/exhaustive-deps

Quan sentis la temptació d'escriure aquesta línia, el problema real sol ser un d'aquests tres: la fàbrica fa alguna cosa que hauria d'estar en un efecte, la dependència s'hauria d'estabilitzar al component pare, o el valor no hauria d'estar a l'estat.

  1. Quan una dependència canvia massa

Aquest és el cas difícil: has posat les dependències correctes i una d'elles canvia a cada render, amb la qual cosa la memoïtzació mai no encerta. Quatre sortides, ordenades de millor a pitjor:

1. Treure el que no depèn de res fora del component.

// ✅ Constants i funcions pures: a l'àmbit del mòdul
const TIPUS_PERMESOS = ['urbana', 'electrica', 'carga'];
const FORMAT_EURO = new Intl.NumberFormat('ca-ES', { style: 'currency', currency: 'EUR' });

function calcularImport(preuHora, hores) {
  return preuHora * hores;
}

function PanellReserva({ bicicleta }) {
  // Ni useMemo ni useCallback: aquests valors no es creen mai més d'una vegada
}

És la millor solució amb diferència: zero memoïtzació, zero dependències, zero risc. Abans de memoïtzar qualsevol cosa, pregunta't si depèn realment d'alguna cosa del component.

2. Moure la funció dins de qui l'usa. Si gestionarEnviament només l'usa un efecte, defineix-la dins de l'efecte i desapareix de l'array de dependències (05-02).

3. Usar useReducer en lloc de diversos useState. La funció despatxar que retorna useReducer és estable per contracte: React garanteix que no canvia entre renders. Això permet passar-la directament a components memoïtzats sense cap useCallback.

// L'estat del formulari de reserva, amb dispatch estable
const [esborrany, despatxar] = useReducer(reductorEsborrany, ESBORRANY_INICIAL);

// `despatxar` és estable: memo funciona sense useCallback
<FormulariReserva esborrany={esborrany} alDespatxar={despatxar} />

El mateix val per a les funcions setX de useState i per al dispatch de react-redux: totes són estables i no necessiten useCallback mai.

4. Usar una referència per a valors que no han de provocar recàlcul. És l'últim recurs, amb precaució, i només per a valors que es llegeixen en gestors o efectes, mai durant el render:

const alReservarRef = useRef(alReservar);
useEffect(() => { alReservarRef.current = alReservar; });   // sempre al dia

const gestionarClic = useCallback((id) => {
  alReservarRef.current(id);      // llegeix la versió més recent sense dependre'n
}, []);                           // identitat estable de per vida

Aquest patró —l'«esdeveniment efectiu»— és útil, però trenca el flux de dades explícit i complica la lectura. Fes-lo servir quan les tres opcions anteriors no serveixin, no abans.

  1. Quan NO memoïtzar

No memoïtzis Per què
Valors primitius (const total = preu * hores) Multiplicar és més barat que consultar la memoïtzació
Càlculs trivials (array.length, una concatenació, un Boolean()) El hook costa més que el càlcul
Filtres sobre arrays curts (menys de ~100 elements i sense feina per element) Recórrer 20 objectes és soroll estadístic
Funcions que només van a un element del DOM (onClick d'un <button>) Al DOM li és igual la identitat de la funció
Funcions que només usa el propi component Ningú les compara
Props d'un component que no està memoïtzat No hi ha cap comparació a aprofitar
Components que es renderitzen una vegada No hi ha renders repetits a estalviar
«Per si de cas» És la definició literal d'optimització prematura

El cas més freqüent i el més inútil de tots:

// ❌ useCallback sense ningú que compari
function PaginaAcces() {
  const gestionarEnviament = useCallback((esdeveniment) => {
    esdeveniment.preventDefault();
    iniciarSessio(usuari);
  }, [usuari]);

  return <form onSubmit={gestionarEnviament}>…</form>;   // un <form> del DOM: no compara res
}

Aquí useCallback afegeix un array de dependències a mantenir, una comparació per render i memòria, a canvi d'exactament res. La versió correcta és una funció normal.

I el compte honest del cost, que rarament es fa explícit: cada useMemo o useCallback suposa una crida al hook, un array creat a cada render, una comparació per dependència i memòria retinguda mentre el component visqui. És poc, però és diferent de zero, i multiplicat per centenars d'usos innecessaris és mesurable.

  1. Alternatives millors que memoïtzar

Abans d'escriure un useMemo, recorre aquesta taula. Cada fila és una tècnica que resol el mateix problema sense dependències a mantenir.

Situació En lloc de memoïtzar Per què és millor
Un estat repinta un subarbre gran Baixar l'estat (08-01) Elimina el render en lloc de saltar-se'l. Menys codi
Un contenidor amb estat embolcalla contingut car Passar children (08-01) Aïllament estructural, sense comparació ni memòria
El valor no depèn de res del component Treure'l fora del component Es crea una vegada en tota l'aplicació
Diversos useState amb gestors que es passen cap avall useReducer despatxar és estable per contracte
Un càlcul car sobre l'estat de Redux createSelector al slice Memoïtzació compartida per tots els components
Un càlcul car sobre dades del servidor select de useQuery Es calcula en arribar la dada, no a cada render
Una llista llarguíssima Paginar o virtualitzar (08-01) Redueix la feina real, no la guarda en caché
El resultat depèn només de l'identificador Índex Map construït una vegada Canvia la complexitat de l'algorisme
Un filtratge que bloqueja l'escriptura useDeferredValue (apartat 14) Reordena la feina en lloc d'evitar-la

L'arbre de decisió complet, que resumeix els apartats 4 a 13:

flowchart TD
    A["Vaig a escriure un useMemo<br/>o un useCallback"] --> B{"Depen d'alguna cosa<br/>del component?"}
    B -->|No| C["Treu-lo FORA del component<br/>constant o funcio pura del modul"]
    B -->|Si| D{"Per que vull<br/>memoitzar?"}

    D -->|"El calcul es car"| E{"L'he mesurat?<br/>console.time + CPU 4x"}
    E -->|No| F["Mesura primer.<br/>Per sota d'1 ms: no memoitzis"]
    E -->|"Si, mes de 5 ms"| G{"Puc reduir la<br/>feina en lloc de guardar-la en cache?"}
    G -->|Si| H["Index Map, Collator reutilitzat,<br/>paginar, select de useQuery"]
    G -->|No| I["useMemo justificat"]

    D -->|"Algu compara<br/>la identitat"| J{"Qui compara?"}
    J -->|Ningu| K["No memoitzis:<br/>cost pur"]
    J -->|"Component amb memo"| L["useCallback / useMemo<br/>i comprova que memo esta posat"]
    J -->|"Dependencia de useEffect"| M["Millor: mou el valor<br/>DINS de l'efecte"]
    J -->|"value d'un context"| N["useMemo + useCallback:<br/>aqui es l'opcio per defecte"]
    J -->|"Selector de Redux"| O["createSelector al slice"]

  1. Hooks de concurrència: useTransition i useDeferredValue

Els dos hooks anteriors intenten fer menys feina. Els de concurrència fan alguna cosa diferent: deixen que React interrompi i posposi feina no urgent perquè la interfície continuï responent. React pot començar a renderitzar una actualització marcada com a no urgent, abandonar-la a mitges si arriba una entrada de l'usuari, atendre aquesta entrada i represa després.

useTransition

const [estaPendent, iniciarTransicio] = useTransition();

Retorna un booleà —«hi ha una transició en curs»— i una funció per embolcallar les actualitzacions no urgents.

// src/components/SelectorTipus.jsx
import { useTransition } from 'react';
import { useSearchParams } from 'react-router';

function SelectorTipus() {
  const [parametres, setParametres] = useSearchParams();
  const [estaPendent, iniciarTransicio] = useTransition();
  const tipus = parametres.get('tipo') ?? 'todos';

  function gestionarCanvi(esdeveniment) {
    const tipusNou = esdeveniment.target.value;

    // No urgent: repintar 2.000 targetes pot esperar i interrompre's
    iniciarTransicio(() => {
      setParametres(tipusNou === 'todos' ? {} : { tipo: tipusNou });
    });
  }

  return (
    <select value={tipus} onChange={gestionarCanvi} aria-busy={estaPendent}>
      <option value="todos">Tots els tipus</option>
      <option value="urbana">Urbana</option>
      <option value="electrica">Elèctrica</option>
      <option value="carga">Càrrega</option>
    </select>
  );
}

El que canvia: sense la transició, triar «Elèctrica» congela el <select> mentre React filtra i pinta el catàleg. Amb ella, el desplegable respon a l'instant, estaPendent permet atenuar la llista mentre es recalcula, i si l'usuari canvia d'opció una altra vegada, React abandona el render a mitges i comença el nou.

Requisit important: l'actualització ha d'anar dins de iniciarTransicio i ser síncrona. I no funciona amb camps de text controlats: el valor d'un <input> és una actualització urgent per definició i marcar-la com a transició produeix un camp que se sent trencat.

useDeferredValue

const valorDiferit = useDeferredValue(valor);

Retorna una còpia del valor que es queda enrere a propòsit. React renderitza primer amb el valor vell (ràpid, urgent) i després, en segon pla i de forma interrompible, amb el nou.

// src/pagines/PaginaCataleg.jsx (fragment)
function PaginaCataleg() {
  const terme = useSelector((estat) => estat.cataleg.terme);
  const termeDiferit = useDeferredValue(terme);
  const estaObsolet = terme !== termeDiferit;

  const visibles = useMemo(
    () => filtrarIOrdenar(bicicletes, estacions, termeDiferit, tipus, ordre),
    [bicicletes, estacions, termeDiferit, tipus, ordre]
  );

  return (
    <>
      <CercadorBicicletes />
      <div style={{ opacity: estaObsolet ? 0.6 : 1, transition: 'opacity 150ms' }}>
        <LlistaBicicletes bicicletes={visibles} />
      </div>
    </>
  );
}

La combinació de useDeferredValue + useMemo + memo és el patró complet: el valor diferit redueix quantes vegades es recalcula durant l'escriptura ràpida, el useMemo evita recalcular quan res de rellevant ha canviat, i el memo de TargetaBicicleta evita tornar a executar les targetes que no s'han mogut. Cap substitueix els altres.

Els tres, comparats

useDebounce (05-06) useTransition useDeferredValue
Quin problema resol Feina massa freqüent Feina urgent barrejada amb no urgent Un valor el consum del qual és car
Mecanisme Temporitzador: espera al silenci Marca l'actualització com a interrompible Renderitza en dues passades, la segona diferible
Retard Fix, el tries tu (300 ms) Cap: s'adapta a la càrrega real Cap de fix: s'adapta
Interrompible No: quan s'executa, bloqueja
Evita peticions al servidor , és el seu punt fort No No
Indicador de progrés Manual estaPendent Comparar valor i valor diferit
On es col·loca Al productor del valor En qui provoca l'actualització En qui consumeix el valor
A CicloUrbano Cercador → petició a json-server SelectorTipus, canvi d'ordre Filtratge local del catàleg

La conclusió pràctica, que és més útil que la taula: no són alternatives, són complementaris. Al CercadorBicicletes definitiu conviuen els dos: useDebounce per no llançar una petició per tecla —això és trànsit de xarxa i el temporitzador és l'eina correcta— i useDeferredValue sobre el terme perquè el filtratge local de les 2.000 bicicletes no bloquegi el camp entre petició i petició.

  1. El React Compiler aquí

El React Compiler de React 19 és, en essència, un generador automàtic de useMemo i useCallback. En un projecte compilat:

Feina La fa el compilador?
Estabilitzar gestionarSeleccio i gestionarReserva a LlistaBicicletes ✅ Sí, sense useCallback
Estabilitzar l'objecte valor de ProveidorTema ✅ Sí, si es construeix en el propi component
Evitar repetir el filtratge i l'ordenació quan les entrades no canvien ✅ Sí, memoïtza l'expressió
Fer que la primera ordenació de 2.000 bicicletes sigui ràpida No. L'algorisme continua sent teu
Substituir el Map d'estacions per un find a cada element No: no reescriu algorismes
Saber que una funció importada d'un altre mòdul és estable No: no raona fora del component
Decidir useTransition o useDeferredValue No: són decisions d'experiència d'usuari
Corregir un useEffect que es dispara de més per una dependència externa No
Optimitzar un component que trenca les regles de React No: l'omet sencer i en silenci

Com comprovar què ha fet. El compilador no és una caixa negra:

  • React DevTools marca els components optimitzats amb una insígnia ✨ Memo ✨ al costat del seu nom. És la comprovació més ràpida: si el teu component crític no la té, el compilador l'ha omès i cal esbrinar per què.
  • El Profiler (08-05) continua sent la prova definitiva: mesura la mateixa interacció amb i sense compilador en un build de producció i compara.
  • El linter (eslint-plugin-react-hooks amb les regles del compilador) et diu per endavant quins components s'ometran, i per què.

I la part que continua sent teva, resumida en una frase: el compilador decideix quan reutilitzar un valor; tu decideixes quin valor val la pena calcular, amb quin algorisme, i si aquesta feina hauria d'estar passant ni tan sols.

Errors Comuns i Consells

Confondre els dos usos legítims. «Memoïtzo perquè és car» i «memoïtzo perquè algú compara la identitat» són raons diferents i porten a decisions diferents. Un useMemo sobre un càlcul trivial és inútil… tret que el resultat sigui un objecte que va a un component memoïtzat, cas en què és imprescindible. Tingues sempre clara quina de les dues raons t'aplica.

useCallback sobre funcions que ningú compara. És l'ús majoritari i el més inútil. Si la funció va a un onClick d'un <button>, treu-lo.

Silenciar exhaustive-deps. Un useMemo amb dependències incompletes produeix dades obsoletes en pantalla: una fallada de correcció, no de rendiment. Si el linter et molesta, el problema és al disseny.

Memoïtzar dins d'un .map(). Els hooks no es poden cridar en bucles (regla de 04-04). Si cada element necessita memoïtzació, la memoïtzació va dins del component fill, no al pare.

Creure que useMemo garanteix l'execució única. React pot descartar el valor guardat. No hi posis res del qual depengui la correcció del teu codi.

Usar useTransition en un camp de text controlat. El valor de l'<input> és urgent per definició; diferir-lo produeix un camp que se sent espatllat. El que es diferix és el seu consum, amb useDeferredValue.

Consell: primer l'algorisme, després la memoïtzació. El Map d'estacions i l'Intl.Collator reutilitzat baixen més el cost que qualsevol useMemo, i continuen funcionant quan les dependències canvien de debò.

Consell: estabilitza des de dalt. Una identitat inestable invalida tota la memoïtzació que hi ha per sota. Arregla el value del proveïdor i l'objecte d'opcions del component arrel abans de tocar les fulles.

Consell: memoïtza el paquet sencer, no les peces. En lloc de tres useMemo per a tres camps, un de sol per a l'objecte que els conté, si és això el que es compararà.

Exercicis

Exercici 1. Per a cada ús, digues si el hook és necessari, sobra o està mal escrit, i corregeix els dos últims casos.

// a)
const total = useMemo(() => hores * bicicleta.preuHora, [hores, bicicleta.preuHora]);

// b)
const bicicletesOrdenades = useMemo(
  () => [...bicicletes].sort((x, y) => x.preuHora - y.preuHora),
  [bicicletes]
);
// bicicletes té 2.000 elements i es passa a <LlistaBicicletes> (memoitzada)

// c)
const gestionarTancament = useCallback(() => setModalObert(false), []);
// Ús: <Modal alTancar={gestionarTancament} />  ← Modal NO està memoitzat

// d)
const filtrades = useCallback(bicicletes.filter((b) => b.estat === 'disponible'), [bicicletes]);

// e)
const valor = useMemo(() => ({ usuari, iniciarSessio, tancarSessio }), []);
// Ús: <ContextSessio value={valor}>

Exercici 2. PaginaDetallEstacio torna a demanar les incidències a json-server a cada render, amb la qual cosa la pestanya parpelleja sense parar. Diagnostica la causa exacta i proposa dues solucions: una amb memoïtzació i una altra sense. Indica quina triaries i per què.

function PaginaDetallEstacio() {
  const { estacioId } = useParams();
  const [incidencies, setIncidencies] = useState([]);
  const filtres = { estacioId, estat: 'abierta', ordre: 'fecha' };

  useEffect(() => {
    let ignorar = false;
    carregarIncidencies(filtres).then((dades) => { if (!ignorar) setIncidencies(dades); });
    return () => { ignorar = true; };
  }, [filtres]);

  return <PestanyaIncidencies incidencies={incidencies} />;
}

Exercici 3. El CercadorBicicletes de CicloUrbano ha de complir tres requisits alhora: (1) el camp respon instantàniament a cada tecla; (2) no es llança una petició a json-server per cada tecla; (3) el filtratge local de les 2.000 bicicletes no bloqueja l'escriptura. Escriu la versió que compleix els tres, usant useDebounce, useDeferredValue i useMemo on correspongui, i justifica en una taula quin requisit cobreix cada eina.

Solucions

Solució 1.

a) Sobra. Una multiplicació de dos números. El useMemo costa més que el càlcul, i el resultat és un primitiu que es compara per valor. Correcció: const total = hores * bicicleta.preuHora;.

b) És necessari, per partida doble. Ordenar 2.000 elements és un càlcul de cost real (ús legítim 1) i el resultat és un array la identitat del qual importa per al memo de LlistaBicicletes (ús legítim 2). A més està ben escrit: [...bicicletes] copia abans d'ordenar, perquè sort muta l'array original i mutar les dades de la caché de Query seria un error greu.

c) Sobra. Modal no està memoïtzat, així que ningú compara alTancar: es torna a executar igualment. Correcció: funció normal, const gestionarTancament = () => setModalObert(false);. (Si demà Modal s'embolcallés en memo, el useCallback passaria a ser necessari.)

d) Està mal escrit. useCallback guarda funcions, i aquí se li passa el resultat d'un .filter(): filtrades acaba sent un array, no una funció, i la memoïtzació no fa el que sembla. Correcció:

const filtrades = useMemo(() => bicicletes.filter((b) => b.estat === 'disponible'), [bicicletes]);

e) Està mal escrit, i és l'error més perillós dels cinc. L'array de dependències està buit, així que valor es congela amb l'usuari del primer render: quan algú iniciï sessió, cap consumidor del context se n'assabentarà. Una fallada de correcció disfressada d'optimització. Correcció:

const valor = useMemo(
  () => ({ usuari, iniciarSessio, tancarSessio }),
  [usuari, iniciarSessio, tancarSessio]
);

Amb iniciarSessio i tancarSessio estabilitzades al seu torn amb useCallback al proveïdor.

Solució 2. Causa exacta: filtres és un objecte literal creat a cada render. L'efecte el té com a dependència, Object.is dona false sempre, l'efecte s'executa, setIncidencies provoca un render, el render crea un altre filtres… i el cicle no s'acaba. És un bucle de peticions, no un parpelleig casual.

Solució A, amb memoïtzació:

const filtres = useMemo(
  () => ({ estacioId, estat: 'abierta', ordre: 'fecha' }),
  [estacioId]
);

Solució B, sense memoïtzació: l'objecte no té per què existir fora de l'efecte.

useEffect(() => {
  let ignorar = false;
  carregarIncidencies({ estacioId, estat: 'abierta', ordre: 'fecha' })
    .then((dades) => { if (!ignorar) setIncidencies(dades); });
  return () => { ignorar = true; };
}, [estacioId]);       // només un primitiu

Triaria la B, per tres raons: no afegeix cap hook ni cap array de dependències a mantenir, l'única dependència és un primitiu (impossible de trencar per identitat) i expressa millor la intenció —«quan canviï l'estació, torna a carregar»—. La regla general: quan una dependència inestable trenca un efecte, la primera pregunta és per què és fora de l'efecte.

I la solució realment correcta al CicloUrbano d'avui, després de 07-06: això no hauria de ser un useEffect. És estat del servidor, i li correspon una consulta amb la seva clau jeràrquica:

const { data: incidencies = [] } = useQuery({
  queryKey: claus.estacions.incidencies(estacioId),
  queryFn: () => carregarIncidencies({ estacioId, estat: 'abierta', ordre: 'fecha' })
});

Solució 3.

// src/components/CercadorBicicletes.jsx (versió definitiva)
import { useState, useEffect, useDeferredValue } from 'react';
import { useDispatch } from 'react-redux';
import { useDebounce } from '../hooks/useDebounce.js';
import { termeCanviat } from '../magatzem/sliceCataleg.js';

function CercadorBicicletes() {
  const [text, setText] = useState('');              // (1) respon a cada tecla
  const termeRetardat = useDebounce(text, 300);       // (2) una petició per pausa
  const despatxar = useDispatch();

  useEffect(() => {
    despatxar(termeCanviat(termeRetardat));
  }, [termeRetardat, despatxar]);

  return (
    <input
      type="search"
      value={text}
      onChange={(esdeveniment) => setText(esdeveniment.target.value)}
      aria-label="Cercar bicicletes per model"
    />
  );
}
// src/pagines/PaginaCataleg.jsx (fragment)
const terme = useSelector((estat) => estat.cataleg.terme);
const termeDiferit = useDeferredValue(terme);            // (3) no bloqueja l'escriptura
const estaObsolet = terme !== termeDiferit;

const visibles = useMemo(                                 // (3) i estabilitat d'identitat
  () => filtrarIOrdenar(bicicletes, estacions, termeDiferit, tipus, ordre),
  [bicicletes, estacions, termeDiferit, tipus, ordre]
);
Eina Requisit que cobreix Per què no serveix una altra
useState local al cercador (1) El camp respon a cada tecla És estat d'interfície pur; cap altra eina hi ha d'intervenir aquí
useDebounce (2) Una petició per pausa, no per tecla Només un temporitzador evita trànsit de xarxa; ni useDeferredValue ni useTransition redueixen peticions
useDeferredValue (3) El filtratge no bloqueja l'escriptura És interrompible i sense retard fix; un segon useDebounce afegiria latència percebuda
useMemo (3) No repetir el càlcul, i estabilitzar visibles El filtratge de 2.000 elements és car i el seu resultat alimenta un component memoïtzat
memo a TargetaBicicleta (3) No tornar a executar les targetes que no canvien Tanca la cadena: sense ell, l'array nou repinta les 2.000 igualment

Conclusió

useMemo(fabrica, dependencies) guarda el valor que produeix la fàbrica i només la torna a executar quan alguna dependència canvia segons Object.is; useCallback(funcio, dependencies) és literalment useMemo(() => funcio, deps) i guarda la funció. La fàbrica s'executa durant el render, així que ha de ser pura, i la memoïtzació és una optimització, no una garantia: React pot descartar el valor guardat, i res del que depengui la correcció del teu codi hi pot recolzar.

El que ordena tota la resta són els dos usos legítims, que cal mantenir sempre separats. El primer és evitar un càlcul realment costós: filtrar, indexar i ordenar 2.000 bicicletes amb Intl.Collator, mesurat abans amb console.time i amb la CPU ralentitzada 4×, amb la referència dels 16 ms del fotograma i la disciplina de treure el useMemo i tornar a mesurar. I amb la lliçó de fons: el Map d'estacions i el collator reutilitzat baixen més el cost que el propi useMemo, perquè canvien l'algorisme en lloc de guardar en caché el resultat. El segon ús és mantenir la identitat d'un valor quan algú la compara: un component embolcallat en memo, un array de dependències de useEffect, el value d'un proveïdor de context o un selector de Redux.

Amb això s'han tancat dues deutes del curs. La de 08-02: LlistaBicicletes estabilitza gestionarSeleccio i gestionarReserva amb useCallback([navegar]), i només llavors el memo de TargetaBicicleta comença a servir d'alguna cosa —els dos junts, mai per separat—. I la de 07-02: ProveidorTema publica un value estabilitzat amb useMemo i una alternarTema estabilitzada amb useCallback, i ProveidorAvisos combina aquesta memoïtzació amb la divisió en contextos d'estat i accions, de manera que un component que només crida mostrarAvis no es torna a executar mai pels avisos.

Sobre les dependències, la regla no admet matisos: tots els valors reactius que la fàbrica llegeix, ni un més ni un menys. Que en sobri una només costa rendiment; que en falti produeix dades obsoletes en pantalla, que és una fallada de correcció. Per això exhaustive-deps té raó gairebé sempre i silenciar-la és el senyal d'alarma més fiable de l'ecosistema. I quan una dependència canvia massa, hi ha sortides millors que insistir: treure fora del component el que no depèn de res, moure la funció dins de l'efecte, usar useReducer perquè despatxar és estable per contracte —igual que setEstat i el dispatch de Redux— i, com a últim recurs, la referència sempre al dia.

També saps quan no memoïtzar: primitius, càlculs trivials, llistes curtes, funcions que només van a un element del DOM, props de components no memoïtzats, i el «per si de cas», que és la definició literal d'optimització prematura. I què fer en el seu lloc: baixar l'estat, passar children, treure constants fora, createSelector, select de useQuery, paginar o virtualitzar. Finalment, els hooks de concurrència, que ataquen el mateix símptoma des d'un altre angle: useTransition marca com a interrompible l'actualització que tu provoques —el canvi de tipus o d'ordre del catàleg—, useDeferredValue diferix el consum d'un valor car sense retard fix, i cap substitueix useDebounce, que continua sent l'única de les tres que evita peticions al servidor. Al cercador definitiu conviuen les tres. El React Compiler, per la seva banda, escriu per tu la memoïtzació mecànica i la marca amb una insígnia ✨ a DevTools, però no millora el teu algorisme, no raona sobre valors importats i no decideix per tu quina feina hauria d'estar passant.

Amb això, el catàleg de CicloUrbano ja no repeteix feina innecessària quan l'usuari interactua. Queda l'altre problema que 07-06 va deixar apuntat i que cap memoïtzació toca: que el navegador descarrega tot el JavaScript de l'aplicació —el taller, el detall d'estacions, el formulari de reserva, la biblioteca de gràfiques— abans de mostrar la primera bicicleta. La propera lliçó és Divisió de Codi i Càrrega Mandrosa.

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