A 05-04 vas aprendre el mecanisme del context i a 05-05 el vas combinar amb useReducer, però les dues lliçons van acabar amb el mateix avís ajornat: «el rendiment del context i el seu paper com a estratègia d'estat global es veuen a 07-02». Aquesta és aquesta lliçó. Aquí el context deixa de ser «el truc per no passar props» i passa a ser el que realment és: una estratègia de gestió d'estat amb un patró d'ús definit, un cost de rendiment mesurable i un límit clar. Veuràs el patró complet d'un mòdul de context per domini, entendràs de debò per què un canvi en un proveïdor repinta tots els seus consumidors encara que només els interessi una part del valor, aplicaràs les tres solucions conegudes —dividir contextos, estabilitzar el valor i baixar l'estat—, refactoritzaràs els quatre contextos de CicloUrbano i els composaràs en un únic Proveidors, i acabaràs amb la llista honesta del que el context no resol. Aquesta llista és el pont cap a Redux.

Contingut

  1. Recordatori de cinc línies: el mecanisme
  2. El patró complet d'un mòdul de context
  3. ProveidorAvisos al complet, comentat
  4. El problema de rendiment, explicat de debò
  5. Per què un objecte literal com value canvia a cada render
  6. Solució 1: dividir contextos per freqüència de canvi
  7. Solució 2: estabilitzar el valor amb memoització
  8. Solució 3: baixar l'estat o passar children
  9. Les tres solucions comparades
  10. Refactor de CicloUrbano i el component Proveidors
  11. Fins on arriba el context: el que no resol
  12. Context + useReducer: el «Redux casolà» i en què es queda curt

  1. Recordatori de cinc línies: el mecanisme

Sense repetir 05-04, això és tot el que cal tenir present:

  • createContext(valorPerDefecte) crea un canal; el proveïdor <ElMeuContext value={…}> publica un valor en un subarbre.
  • useContext(ElMeuContext) cerca el proveïdor més proper cap amunt i en retorna el valor; si no n'hi ha cap, retorna el valor per defecte.
  • A React 19 el mateix context s'usa com a proveïdor: <ContextTema value={valor}>, sense .Provider.
  • El context no és estat: és un mecanisme de transport. L'estat continua sent-hi, en un useState o un useReducer dins del proveïdor.
  • Quan el valor publicat canvia, tots els components que fan useContext d'aquest context es tornen a executar.

Aquesta última línia és la que es desenvolupa en aquesta lliçó, perquè conté tot el cost.

  1. El patró complet d'un mòdul de context

Un context ben fet no és una crida a createContext solta: és un mòdul per domini amb cinc peces sempre en el mateix ordre. És l'estructura que ja segueixen ContextUsuari i ContextTema, formalitzada.

Peça Què és Regla
El context const ContextX = createContext(null) No s'exporta. Així ningú es pot saltar el hook d'accés
L'estat useState o useReducer dins del proveïdor N'és el propietari real
El valor exposat L'objecte que es publica Es decideix deliberadament: dades, derivats i accions
El proveïdor export function ProveidorX({ children }) Component propi, al seu fitxer, amb el seu CSS si el necessita
El hook d'accés export function useX() Llança si el context és null: falta el proveïdor
// src/contextos/ContextExemple.jsx — l'esquelet del patró
import { createContext, useContext, useState } from 'react';

// 1) El context: privat del mòdul
const ContextExemple = createContext(null);

// 2) i 3) i 4) El proveïdor: propietari de l'estat i del valor
export function ProveidorExemple({ children }) {
  const [valor, setValor] = useState(null);

  const contingut = { valor, establirValor: setValor };

  return <ContextExemple value={contingut}>{children}</ContextExemple>;
}

// 5) El hook d'accés: única porta d'entrada
export function useExemple() {
  const context = useContext(ContextExemple);
  if (context === null) {
    throw new Error('useExemple s\'ha d\'usar dins de <ProveidorExemple>');
  }
  return context;
}

Per què el hook que llança importa més del que sembla: sense ell, un component col·locat fora del proveïdor rebria el valor per defecte —normalment null— i fallaria més tard amb un Cannot read properties of null, en un altre punt del codi i amb un missatge que no diu res. Amb el throw, la fallada apareix exactament on és l'error i amb el nom del proveïdor que falta.

Un fitxer per domini. No un ContextGlobal.jsx amb la sessió, el tema i els avisos junts: això és un únic proveïdor el valor del qual canvia cada vegada que canvia qualsevol dels tres, i és l'origen del problema de l'apartat 4.

  1. ProveidorAvisos al complet, comentat

Aquest és el mòdul d'avisos de CicloUrbano escrit amb el patró sencer. Apareixia esbossat a 05-04; aquí hi ha la versió que es queda al projecte.

// src/contextos/ContextAvisos.jsx
import { createContext, useContext, useState, useCallback } from 'react';
import Avis from '../components/Avis.jsx';
import estils from './ContextAvisos.module.css';

const ContextAvisos = createContext(null);

const DURACIO_PER_DEFECTE = 5000;

export function ProveidorAvisos({ children }) {
  // L'estat real: una pila de { id, to, text }
  const [avisos, setAvisos] = useState([]);

  // useCallback manté la identitat de la funció entre renders.
  // El detall de per què funciona és a 08-03; aquí n'hi ha prou de saber
  // que sense ell aquestes funcions serien noves a cada render.
  const descartarAvis = useCallback((id) => {
    setAvisos((previs) => previs.filter((avis) => avis.id !== id));
  }, []);

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

    if (duracio > 0) {
      setTimeout(() => {
        setAvisos((previs) => previs.filter((avis) => avis.id !== id));
      }, duracio);
    }
    return id;
  }, []);

  const valor = { avisos, mostrarAvis, descartarAvis };

  return (
    <ContextAvisos value={valor}>
      {children}
      <div className={estils.pila} role="status" aria-live="polite">
        {avisos.map((avis) => (
          <Avis
            key={avis.id}
            to={avis.to}
            text={avis.text}
            alTancar={() => descartarAvis(avis.id)}
          />
        ))}
      </div>
    </ContextAvisos>
  );
}

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

Decisions que convé justificar:

  • setAvisos amb funció actualitzadora a les tres crides. És imprescindible: un setTimeout que es dispara cinc segons més tard no es pot refiar dels avisos capturats en aquell render (05-01).
  • L'identificador es genera al proveïdor, no el passa qui crida. Qui mostra un avís no s'hauria de preocupar d'això.
  • La pila es pinta dins del proveïdor, després de {children}. Així qualsevol component de l'arbre pot llançar avisos i hi ha un únic lloc on es pinten.
  • role="status" amb aria-live="polite" (03-06): els avisos apareixen sense moure el focus, així que s'han d'anunciar als lectors de pantalla.
  • useCallback a les dues accions. És la primera peça de la solució al problema de rendiment i el motiu pel qual a l'apartat 6 es pot separar el context d'accions.

  1. El problema de rendiment, explicat de debò

Aquí hi ha el cost real del context, i convé enunciar-lo amb precisió perquè es repeteix malament sovint.

Quan el valor d'un proveïdor canvia, React torna a executar tots els components que consumeixen aquest context, encara que només els interessi una part del valor que no ha canviat.

No hi ha selecció granular. useContext és «subscriu-me al valor sencer». Si el valor és { avisos, mostrarAvis, descartarAvis } i només canvia avisos, un component que únicament usa mostrarAvis es torna a executar igualment.

Vegem el cas concret de CicloUrbano. ContextReserves publica { estat, despatxar }, i estat conté reserves, esborrany, estatEnviament i error. L'esborrany canvia a cada pulsació de tecla del formulari de reserva.

flowchart TD
    A["L'usuari escriu una lletra<br/>a FormulariReserva"] --> B["despatxar esborrany_actualitzat"]
    B --> C["reductorReserves retorna<br/>un estat nou"]
    C --> D["ProveidorReserves s'executa<br/>i publica un valor nou"]
    D --> E["PanellReserves<br/>usa reserves"]
    D --> F["Capcalera<br/>usa el comptador de reserves"]
    D --> G["PaginaReserves<br/>usa reserves"]
    D --> H["FormulariReserva<br/>usa esborrany"]
    D --> I["DialegReserva<br/>només usa despatxar"]
    E --> J["Repintat innecessari"]
    F --> J
    G --> J
    I --> J
    H --> K["Repintat necessari"]
    style J fill:#fde68a,stroke:#b45309
    style K fill:#bbf7d0,stroke:#12805c

Quatre dels cinc consumidors s'han tornat a executar sense que res del que llegeixen hagi canviat. Multiplica-ho per les lletres d'«Elèctrica Pro» i tens tretze rondes de repintat de mitja aplicació per escriure un model.

Un matís important per no exagerar el problema: tornar a executar un component no és el mateix que tocar el DOM. React executa la funció, compara el resultat amb l'anterior i només aplica al DOM el que ha canviat (01-05). Si els components són barats, no notaràs res. El problema apareix quan els consumidors són cars —llistes llargues, càlculs, subarbres grans— o quan el valor canvia molt sovint. I al catàleg de CicloUrbano, amb LlistaBicicletes pintant targetes, es compleixen les dues condicions.

  1. Per què un objecte literal com value canvia a cada render

Hi ha un segon problema, més subtil i molt fàcil de patir sense adonar-se'n: el valor pot «canviar» sense que canviï l'estat.

export function ProveidorUsuari({ children }) {
  const [usuari, setUsuari] = useState(null);

  function iniciarSessio(id) { /* … */ }
  function tancarSessio() { setUsuari(null); }

  // ⚠️ Objecte literal nou a CADA render del proveïdor
  const valor = {
    usuari,
    esOperari: usuari?.rol === 'operario',
    iniciarSessio,
    tancarSessio
  };

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

React decideix si ha d'avisar els consumidors comparant el valor nou amb l'anterior mitjançant Object.is, és a dir, per identitat de referència. I { usuari, … } construeix un objecte nou cada vegada que s'executa la funció. Encara que usuari sigui exactament el mateix objecte, valor és una referència diferent i tots els consumidors es repinten.

El mateix passa amb iniciarSessio i tancarSessio: són funcions redeclarades a cada render, amb identitat nova cada vegada.

Quan es torna a executar el proveïdor? Sempre que es torna a executar el seu pare. I aquí hi ha el parany: si ProveidorUsuari és dins d'un component que es repinta per qualsevol motiu aliè —un canvi de ruta, un canvi de tema—, publicarà un valor nou, i tots els consumidors de la sessió es repintaran sense que la sessió hagi canviat en absolut.

// Comparació d'identitats entre dos renders del proveïdor
// Render 1: valor === { usuari: usr01, esOperari: false, … }   ← referència A
// Render 2: valor === { usuari: usr01, esOperari: false, … }   ← referència B
// Object.is(A, B) → false  ⇒  React avisa tots els consumidors

Aquest és el fallo que arregla la solució 2. Però abans, la que dona més rendiment per menys codi.

  1. Solució 1: dividir contextos per freqüència de canvi

La idea és senzilla i molt eficaç: si un valor té parts que canvien a ritmes diferents, publica-les en contextos diferents. Cada consumidor se subscriu només al que necessita.

A ContextReserves hi ha una divisió evident i gratuïta: estat canvia constantment, mentre que despatxar és estable per garantia de React —ho vas veure a 05-05: useReducer retorna sempre la mateixa funció despatxar durant tota la vida del component—. Separar-los converteix tots els components que només despatxen en immunes als canvis d'estat.

// src/contextos/ContextReserves.jsx
import { createContext, useContext, useReducer } from 'react';
import { reductorReserves, ESTAT_INICIAL_RESERVES } from '../reductors/reserves.js';

// Dos contextos: un per al que canvia i un altre per al que no
const ContextReservesEstat = createContext(null);
const ContextReservesAccions = createContext(null);

export function ProveidorReserves({ children }) {
  const [estat, despatxar] = useReducer(reductorReserves, ESTAT_INICIAL_RESERVES);

  return (
    // despatxar és ESTABLE: aquest proveïdor mai publica un valor nou
    <ContextReservesAccions value={despatxar}>
      <ContextReservesEstat value={estat}>
        {children}
      </ContextReservesEstat>
    </ContextReservesAccions>
  );
}

/** Llegeix l'estat de reserves. Es repinta quan l'estat canvia. */
export function useReservesEstat() {
  const context = useContext(ContextReservesEstat);
  if (context === null) {
    throw new Error('useReservesEstat s\'ha d\'usar dins de <ProveidorReserves>');
  }
  return context;
}

/** Obté despatxar. MAI provoca un repintat per canvi d'estat. */
export function useReservesAccions() {
  const context = useContext(ContextReservesAccions);
  if (context === null) {
    throw new Error('useReservesAccions s\'ha d\'usar dins de <ProveidorReserves>');
  }
  return context;
}

Punts clau d'aquesta versió:

  • El context d'accions publica despatxar directament, no { despatxar }. Un objecte embolcallador tornaria a introduir el problema d'identitat de l'apartat 5, precisament el que estem evitant.
  • L'ordre d'imbricació importa poc funcionalment, però el d'accions va per fora per claredat: és el que mai canvia.
  • Es manté el hook que llança, ara duplicat. És repetició acceptable: cada hook documenta el seu cost en el seu propi nom.

L'efecte sobre el diagrama de l'apartat 4 és directe: DialegReserva, que només despatxa, deixa de repintar-se en escriure al formulari. I en un component que despatxa des d'un gestor —el cas més habitual— la millora es multiplica per tants consumidors «només escriptors» com tingui l'aplicació.

La divisió també s'aplica per domini, no només per estat/accions. Si avui tinguessis un únic ContextGlobal amb sessió, tema i avisos, cada avís repintaria els lectors de la sessió. Que CicloUrbano tingui quatre contextos separats des de 05-04 ja és, de fet, una aplicació d'aquesta regla.

Compatibilitat amb el que ja està escrit. Si prefereixes no tocar els consumidors actuals de cop, mantén un useReserves() de conveniència mentre duri la migració:

/** Compatibilitat: retorna { estat, despatxar } com abans.
 *  Se subscriu als DOS contextos, així que no estalvia repintats.
 *  Usa'l només mentre migres els components. */
export function useReserves() {
  return { estat: useReservesEstat(), despatxar: useReservesAccions() };
}

És honest reconèixer el que fa: no optimitza res. Serveix per migrar per parts, i hauria de desaparèixer quan l'últim component usi el hook granular.

  1. Solució 2: estabilitzar el valor amb memoització

La segona solució ataca el problema de l'apartat 5: evitar que el valor canviï d'identitat quan el seu contingut no ha canviat.

// src/contextos/ContextUsuari.jsx — valor estabilitzat
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
import { usuaris } from '../dades/domini.js';

const ContextUsuari = createContext(null);

export function ProveidorUsuari({ children }) {
  const [usuari, setUsuari] = useState(null);
  const [carregantSessio, setCarregantSessio] = useState(true);

  // Identitat estable: no es redeclaren en cada render
  const iniciarSessio = useCallback((id) => {
    const trobat = usuaris.find((candidat) => candidat.id === id);
    if (trobat) setUsuari(trobat);
  }, []);

  const tancarSessio = useCallback(() => setUsuari(null), []);

  // L'objecte només es reconstrueix si canvia usuari o carregantSessio
  const valor = useMemo(
    () => ({
      usuari,
      esOperari: usuari?.rol === 'operario',
      carregantSessio,
      iniciarSessio,
      tancarSessio
    }),
    [usuari, carregantSessio, iniciarSessio, tancarSessio]
  );

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

Què aconsegueix: si ProveidorUsuari es torna a executar per un motiu aliè a la sessió, useMemo retorna el mateix objecte d'abans, Object.is dona true i React no avisa cap consumidor.

Què no aconsegueix: si la sessió sí que canvia, tots els consumidors es repinten igual, inclosos els que només usen tancarSessio. Estabilitzar evita els repintats espuris; no dona selecció granular. Per a això hi ha la solució 1.

useMemo i useCallback s'estudien a fons a 08-03: què memoïtzen exactament, quan compensen i quan són soroll. Aquí n'hi ha prou amb la regla operativa:

Tot proveïdor que publiqui un objecte literal com a value l'ha d'estabilitzar amb useMemo, i les funcions que aquest objecte contingui, amb useCallback. És dels pocs llocs on memoïtzar és l'opció per defecte i no una optimització prematura.

  1. Solució 3: baixar l'estat o passar children

La tercera solució no toca el context: canvia l'estructura perquè menys components en depenguin.

8.1 Baixar l'estat

Si un estat que viu en un proveïdor l'usa en realitat una zona petita de l'arbre, baixa'l. És el «baixar» de l'apartat 5 de 07-01, aplicat a un context.

// ABANS: el terme de cerca en un context que llegeix tota l'aplicació
// DESPRÉS: al component que l'usa
function CercadorBicicletes({ alBuscar }) {
  const [terme, setTerme] = useState('');
  const termeRetardat = useDebounce(terme, 400);
  // …
}

Si la dada no necessita estar a dalt, treure-la del context elimina el problema en lloc de mitigar-lo. És l'única de les tres solucions que redueix complexitat en lloc d'afegir-ne.

8.2 Passar children per aïllar subarbres

Aquest truc és menys conegut i molt potent. Un component que es torna a executar no obliga a re-executar els fills que ha rebut com a children, perquè aquests elements ja venien creats des de fora: la seva identitat no ha canviat.

// ABANS: el proveïdor construeix el seu contingut, així que tot es re-executa amb ell
function ProveidorTema() {
  const [tema, setTema] = useState('clar');
  return (
    <ContextTema value={{ tema, setTema }}>
      <Capcalera />
      <Cataleg />   {/* es re-executa amb cada canvi de tema, l'usi o no */}
    </ContextTema>
  );
}

// DESPRÉS: el contingut arriba des de fora i no es re-executa en canviar el tema
function ProveidorTema({ children }) {
  const [tema, setTema] = useState('clar');
  const valor = useMemo(() => ({ tema, setTema }), [tema]);
  return <ContextTema value={valor}>{children}</ContextTema>;
}

Amb la segona versió, un canvi de tema re-executa ProveidorTema, però children és el mateix element de React que ja existia: React se'l salta i només es re-executen els components que realment consumeixen ContextTema. Aquesta és la raó tècnica per la qual un proveïdor ha de rebre sempre children i no construir el seu contingut, i per la qual el patró de l'apartat 2 ho exigeix.

  1. Les tres solucions comparades

1. Dividir contextos 2. Estabilitzar amb memoització 3. Baixar l'estat / children
Quin problema resol Consumidors repintats per parts del valor que no usen Repintats espuris per identitat nova sense canvi real Que hi hagi consumidors de més
Esforç Mitjà: dos contextos i dos hooks per domini Baix: useMemo + useCallback al proveïdor Variable: pot exigir reestructurar
Guany Alt quan hi ha molts «només escriptors» Mitjà: elimina el soroll, no la cascada real El més gran: el problema desapareix
Risc Més fitxers i més hooks a recordar Dependències mal declarades a useMemo Reintroduir prop drilling si t'excedeixes
Quan usar-la Estat que canvia sovint + accions estables Sempre en qualsevol proveïdor amb objecte literal Quan la dada no necessitava estar a dalt

No són excloents: a CicloUrbano s'apliquen les tres alhora. La 2 és obligatòria a tots els proveïdors, la 1 només als que tenen un valor de ritmes mixtos, i la 3 és la revisió que es fa en classificar l'estat (07-01).

  1. Refactor de CicloUrbano i el component Proveidors

Amb les regles aplicades, així queda el mapa de contextos.

Context Estat Freqüència de canvi Tractament
ContextTema tema Molt baixa useMemo al valor. No cal dividir
ContextUsuari usuari, carregantSessio Molt baixa useMemo + useCallback. No cal dividir
ContextAvisos avisos Mitjana useCallback a les accions + divisió estat/accions
ContextReserves Estat del reductor Alta (cada tecla) Divisió obligatòria: estat i despatxar separats

I el problema que queda: main.jsx i Disseny acumulen una piràmide de proveïdors imbricats que creix amb cada domini nou. La solució és un component de composició.

// src/contextos/Proveidors.jsx
import { ProveidorTema } from './ContextTema.jsx';
import { ProveidorUsuari } from './ContextUsuari.jsx';

/**
 * Proveïdors globals de CicloUrbano, en el seu ordre correcte.
 * Els que depenen de l'enrutador (avisos, reserves) viuen a Disseny.
 */
export function Proveidors({ children }) {
  return (
    <ProveidorTema>
      <ProveidorUsuari>
        {children}
      </ProveidorUsuari>
    </ProveidorTema>
  );
}
// src/main.jsx — amb la composició aplicada
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router';
import { router } from './rutes.jsx';
import LimitError from './components/LimitError.jsx';
import { Proveidors } from './contextos/Proveidors.jsx';
import { registrarError } from './utilitats/monitoritzacio.js';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <LimitError titol="CicloUrbano no està disponible en aquest moment" alRegistrar={registrarError}>
      <Proveidors>
        <RouterProvider router={router} />
      </Proveidors>
    </LimitError>
  </StrictMode>
);
flowchart TD
    A["StrictMode"] --> B["LimitError"]
    B --> C["Proveidors<br/>(Tema + Usuari)"]
    C --> D["RouterProvider"]
    D --> E["Disseny (ruta arrel)"]
    E --> F["ProveidorReserves<br/>estat + accions separats"]
    F --> G["ProveidorAvisos"]
    G --> H["Capcalera · Outlet · PeuDePagina"]

Dues regles de col·locació que ja coneixies de 06-02 i 06-03 i que la composició no ha de trencar:

  • ProveidorTema i ProveidorUsuari van fora de l'enrutador, perquè han de sobreviure a absolutament tot, inclosa la pantalla d'error de ruta.
  • ProveidorReserves i ProveidorAvisos van dins de Disseny, la ruta arrel, perquè pertanyen al marc de l'aplicació i Disseny no es desmunta en navegar.

L'ordre d'imbricació té una regla addicional: si un proveïdor consumeix un altre, va per dins. ProveidorReserves podria necessitar l'usuari per associar la reserva; l'usuari, en canvi, no necessita res de les reserves. Per això l'usuari va per fora.

  1. Fins on arriba el context: el que no resol

El context és una eina excel·lent per al que fa, i no cal substituir-lo per costum. Però té un sostre, i aquestes són les cinc coses concretes que no aporta.

Mancança Què significa a la pràctica
Eines de depuració No hi ha un panell que t'ensenyi què va canviar, quan i qui ho va provocar. La depuració és console.log i el Profiler
Middleware No hi ha un punt únic on interceptar tots els canvis per registrar, mesurar, persistir o comprovar permisos. Cal repetir-ho a cada proveïdor
Viatge en el temps No pots retrocedir a l'estat anterior per reproduir una fallada. Ni desfer/refer sense implementar-ho sencer
Selecció granular useContext et subscriu al valor sencer. Dividir contextos mitiga, no resol: no existeix «subscriu-me només a estat.reserves»
Lògica asíncrona organitzada Cada proveïdor resol les seves peticions a la seva manera. No hi ha convenció compartida per a càrrega, error i cancel·lació

Els senyals que ha arribat el moment de plantejar-se una biblioteca dedicada:

  1. Ja has dividit els contextos i continues veient repintats que no haurien d'ocórrer.
  2. Tens més de sis o vuit proveïdors i l'ordre d'imbricació entre ells comença a importar de maneres subtils.
  3. Necessites saber què va provocar un canvi d'estat, i console.log al reductor s'ha quedat curt.
  4. Diversos proveïdors necessiten reaccionar al mateix succés —«s'ha tancat la sessió, neteja reserves, avisos i esborranys»— i estàs passant funcions d'un proveïdor a un altre.
  5. L'equip ha crescut i fa falta una convenció comuna, no que cada domini inventi el seu patró.
  6. Vols persistir part de l'estat, o registrar cada canvi per diagnòstic, i no vols escriure-ho quatre vegades.

Si no et reconeixes en cap, el context t'és suficient i afegir Redux seria globalització prematura (07-01). Si et reconeixes en tres o més, continua llegint el mòdul.

  1. Context + useReducer: el «Redux casolà» i en què es queda curt

A 05-05 es va dir que el model de Redux és exactament el de useReducer, i és literalment cert. Amb useReducer + context ja tens:

  • Un estat centralitzat per domini (el del reductor).
  • Canvis només per accions descriptives, no per assignacions soltes.
  • Un reductor pur que concentra totes les transicions en un lloc i es prova sense React.
  • Un despatxar estable disponible a tot l'arbre.

Aquests són els tres principis de Redux, que veuràs enunciats a la propera lliçó. La diferència no és conceptual, és d'infraestructura:

Context + useReducer Redux Toolkit
Model mental Idèntic Idèntic
Abast Un reductor per domini, cadascun en el seu proveïdor Un magatzem únic amb diversos reductors combinats
Subscripció Al valor sencer del context Per selector, amb comparació per identitat
Depuració Manual DevTools amb historial i viatge en el temps
Interceptar canvis No hi ha punt únic Middleware
Asincronia Cadascú la resol com pot createAsyncThunk amb un patró comú
Viu fora de React No: l'estat és a l'arbre Sí: el magatzem és un objecte independent
Cost Zero dependències Una dependència i un vocabulari

L'última fila de la comparació és més important del que sembla. En context + useReducer, l'estat viu dins de l'arbre de React: si el proveïdor es desmunta, l'estat desapareix, i res que no sigui un component el pot llegir. El magatzem de Redux és un objecte JavaScript normal que existeix al marge de React; es pot llegir des d'una utilitat, des d'un interceptor de peticions o des d'una prova, sense muntar res.

Dit això, i per acabar amb honestedat: context + useReducer és una solució perfectament vàlida per a moltíssimes aplicacions, i CicloUrbano funcionaria bé així indefinidament. El que ve a continuació al mòdul no és una correcció d'un error: és el que guanyes quan l'aplicació i l'equip creixen.

Errors Comuns i Consells

Error 1: publicar un objecte literal sense useMemo. És el fallo més estès i el més silenciós, perquè l'aplicació funciona: simplement va més lenta del que hauria i ningú sap per què. Regla fixa: objecte literal a valueuseMemo.

Error 2: un únic ContextGlobal per a tot. Ajunta dades amb freqüències de canvi incompatibles i garanteix que un avís repinti els lectors de la sessió. Un fitxer per domini.

Error 3: exportar el context a més del hook. Tan bon punt està exportat, algú l'usarà amb useContext directament i se saltarà la validació del proveïdor. Mantén-lo privat del mòdul.

Error 4: que el proveïdor construeixi el seu contingut en lloc de rebre children. Anul·la l'aïllament de l'apartat 8.2 i fa que tot el subarbre es re-executi amb cada canvi del proveïdor.

Error 5: embolcallar despatxar en un objecte. value={{ despatxar }} crea una referència nova a cada render i llança per la borda l'estabilitat que React et regalava. Publica'l tal qual.

Error 6: creure que dividir contextos dona selecció granular. Divideix per ritmes de canvi, no en cent contextos d'un camp cadascun. Si necessites de debò subscriure't a un camp concret d'un objecte gran, aquest és exactament el senyal 1 de l'apartat 11.

Consell 1: anomena els hooks pel seu cost. useReservesEstat i useReservesAccions diuen en el seu nom a què et subscrius. Qui llegeix el component sap si es repintarà.

Consell 2: comprova l'efecte amb el Profiler. Abans i després de dividir un context, grava amb React DevTools Profiler (08-05) i compara. És l'única manera de saber si el refactor ha servit d'alguna cosa.

Consell 3: no divideixis per endavant. Comença amb un context per domini i el valor memoïtzat. Divideix en estat i accions quan l'estat comenci a canviar sovint, no abans.

Exercicis

Exercici 1. Aquest proveïdor té quatre problemes dels vistos a la lliçó. Identifica'ls i reescriu-lo aplicant el patró complet.

// src/contextos/ContextTema.jsx
import { createContext, useState } from 'react';

export const ContextTema = createContext('clar');

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

  function alternarTema() {
    setTema(tema === 'clar' ? 'fosc' : 'clar');
  }

  return (
    <ContextTema value={{ tema, alternarTema }}>
      <Capcalera />
      <Cataleg />
      <PeuDePagina />
    </ContextTema>
  );
}

Exercici 2. Divideix ContextAvisos en estat i accions seguint el patró de ContextReserves, i explica quins components de CicloUrbano hi guanyen amb la divisió. Tingues en compte que mostrarAvis i descartarAvis són dues funcions, no una: pensa com publicar el context d'accions sense reintroduir el problema d'identitat.

Exercici 3. A CicloUrbano, Capcalera mostra un distintiu amb el nombre de reserves actives i MenuUsuari mostra el nom de l'usuari. Amb la implementació actual, escriure una lletra a FormulariReserva repinta les dues. Explica per què passa amb cadascuna i proposa la solució concreta per a cada cas.

Solucions

Solució 1. Els quatre problemes:

  1. El context està exportat (export const ContextTema), així que qualsevol el pot usar amb useContext saltant-se la validació.
  2. No hi ha hook d'accés que llanci si falta el proveïdor. I el valor per defecte és 'clar', una cadena, mentre que el valor real és un objecte: qui l'usi fora del proveïdor rebrà 'clar' i fallarà en fer tema.tema, amb un error incomprensible.
  3. El proveïdor construeix el seu contingut en lloc de rebre children, així que cada canvi de tema re-executa Capcalera, Cataleg i PeuDePagina encara que no consumeixin el context.
  4. El valor és un objecte literal sense memoïtzar, i alternarTema es redeclara a cada render. A més, alternarTema llegeix tema de la clausura en lloc d'usar la funció actualitzadora.
// src/contextos/ContextTema.jsx — corregit
import { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';

const ContextTema = createContext(null);   // 1) privat del mòdul

export function ProveidorTema({ children }) {   // 3) rep children
  const [tema, setTema] = useState('clar');

  // 4) identitat estable + funció actualitzadora
  const alternarTema = useCallback(() => {
    setTema((previ) => (previ === 'clar' ? 'fosc' : 'clar'));
  }, []);

  // Sincronitza l'atribut que usen les variables CSS de index.css
  useEffect(() => {
    document.documentElement.dataset.tema = tema;
  }, [tema]);

  // 4) valor estabilitzat
  const valor = useMemo(() => ({ tema, alternarTema }), [tema, alternarTema]);

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

// 2) hook d'accés que llança
export function useTema() {
  const context = useContext(ContextTema);
  if (context === null) {
    throw new Error('useTema s\'ha d\'usar dins de <ProveidorTema>');
  }
  return context;
}

Solució 2. La clau és a l'enunciat: com que hi ha dues funcions, publicar-les com { mostrarAvis, descartarAvis } crearia un objecte nou a cada render. Cal memoïtzar també aquest objecte d'accions, que ara sí que és estable de debò perquè els seus dos membres porten useCallback amb dependències buides.

// src/contextos/ContextAvisos.jsx — dividit
const ContextAvisosEstat = createContext(null);
const ContextAvisosAccions = createContext(null);

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 = DURACIO_PER_DEFECTE) => {
    const id = `avis-${crypto.randomUUID().slice(0, 8)}`;
    setAvisos((previs) => [...previs, { id, to, text }]);
    if (duracio > 0) {
      setTimeout(() => {
        setAvisos((previs) => previs.filter((avis) => avis.id !== id));
      }, duracio);
    }
    return id;
  }, []);

  // Objecte memoïtzat amb [] efectives: mai canvia d'identitat
  const accions = useMemo(
    () => ({ mostrarAvis, descartarAvis }),
    [mostrarAvis, descartarAvis]
  );

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

export function useAvisosEstat() { /* … llança si és null … */ }
export function useAvisosAccions() { /* … llança si és null … */ }

Qui hi guanya amb la divisió: tots els components que només llancen avisos i mai els llegeixen, que a CicloUrbano són gairebé tots —FormulariReserva, PaginaNovaReserva, PanellReserves, PaginaAcces, DialegReserva—. Abans, cada avís mostrat els repintava a tots; ara només es repinta LlistaAvisos, que és l'únic que llegeix la pila. El guany és notable perquè els avisos apareixen i desapareixen sols amb un temporitzador: cada avís provocava dues rondes de repintat general.

Solució 3. Són dues causes diferents, i per això porten solucions diferents.

Capcalera amb el comptador de reserves. Consumeix ContextReserves per calcular reserves.filter((r) => r.estat === 'activa').length. Com que useContext subscriu al valor sencer i l'esborrany forma part d'aquest valor, cada tecla publica un estat nou i repinta tota la Capcalera —amb el seu nav, els seus NavLink i el seu MenuUsuari—.

Solucions, de menor a major:

  1. Dividir l'estat dins del reductor: publicar reserves i esborrany en contextos diferents, de manera que la Capcalera només se subscrigui a la llista. És la solució 1 portada un pas més enllà i funciona, però es nota que estàs lluitant contra la manca de selecció granular.
  2. Extreure el distintiu al seu propi component DistintiuReserves, que consumeix el context, i deixar que Capcalera no el consumeixi. Així el repintat queda contingut en un <span> en lloc de tota la capçalera. És barat i eficaç.
  3. La solució real és la de l'apartat 11: aquesta necessitat —«vull subscriure'm només a estat.reserves»— és exactament el senyal 1. Amb useSelector (07-05) s'expressa en una línia i sense dividir res.

MenuUsuari amb el nom de l'usuari. Aquí no s'hauria de repintar en absolut, perquè consumeix ContextUsuari i la sessió no ha canviat. Si ho fa, és per una d'aquestes dues causes:

  • El valor de ProveidorUsuari no està memoïtzat (apartat 5): el proveïdor es torna a executar per un motiu aliè i publica un objecte nou. Solució: useMemo + useCallback, com a l'apartat 7.
  • MenuUsuari també consumeix ContextReserves —per exemple, per mostrar «tens 2 reserves actives»—. Aleshores el repintat no ve de la sessió, i la solució és la mateixa que la de la Capcalera.

La conclusió que interessa: Capcalera és un problema estructural del context que només es mitiga; MenuUsuari és un fallo evitable que es corregeix amb memoització. Distingir quin dels dos tens al davant és el que evita refactoritzacions inútils.

Conclusió

El context és una estratègia de gestió d'estat completa quan s'usa amb el patró sencer: un fitxer per domini, el context privat del mòdul, l'estat en un useState o un useReducer dins del proveïdor, un valor exposat que es decideix deliberadament, el proveïdor com a component propi que rep children i un hook d'accés que llança si falta el proveïdor. ProveidorAvisos és l'exemple complet d'aquest patró, i Proveidors és la resposta a la piràmide d'imbricacions que creix amb cada domini nou.

Sobre el cost, ara ja saps què passa de debò: qualsevol consumidor es torna a executar quan canvia el valor del proveïdor, encara que només li interessi una part, perquè useContext no ofereix selecció granular; i a més el valor pot «canviar» sense canviar, perquè un objecte literal té identitat nova a cada render i React compara amb Object.is. Les tres solucions són complementàries: dividir contextos per freqüència de canvi i separar estat d'accions —aprofitant que despatxar és estable per garantia de React—, estabilitzar el valor amb useMemo i useCallback, que en un proveïdor no és optimització prematura sinó obligació, i baixar l'estat o passar children perquè el subarbre deixi de dependre del proveïdor, que és l'única de les tres que redueix complexitat en lloc d'afegir-ne. A CicloUrbano s'apliquen les tres: tema i usuari memoïtzats, avisos i reserves dividits en estat i accions, i tot compost en un Proveidors que respecta la col·locació de 06-02 i 06-03.

I saps on és el sostre. El context no et dona eines de depuració, ni un punt únic on interceptar canvis, ni viatge en el temps, ni selecció granular, ni una convenció compartida per a la lògica asíncrona. Context + useReducer és, conceptualment, Redux: estat centralitzat, canvis només per accions, reductor pur. El que li falta no és el model, és la infraestructura que es construeix al voltant d'aquest model, començant per un magatzem que viu fora de l'arbre de React i per un panell que t'ensenya quina acció va canviar què. Això és el que arriba ara: què és Redux, quin problema resol exactament, per què Redux Toolkit és avui l'única manera sensata d'usar-lo i com es munta el magatzem de CicloUrbano. La propera lliçó és Redux: Introducció i Configuració.

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