A 04-01 vas elevar l'estat a l'avantpassat comú i el catàleg de CicloUrbano va començar a filtrar de veritat, però la lliçó va acabar posant nom al preu d'aquesta tècnica: la perforació de props. Quan la dada viu a dalt i s'utilitza a baix, ha de travessar tots els components intermedis, que la reben sense usar-la i la reenvien sense entendre-la. Amb dos nivells molesta; amb cinc, cada canvi en la signatura d'un component obliga a recórrer mitja aplicació a mà. useContext és la resposta de React: un canal directe entre un component i qualsevol dels seus descendents, sense escales. En aquesta lliçó veuràs el problema concret a CicloUrbano, les tres peces del mecanisme (createContext, el proveïdor i useContext), com React busca el proveïdor més proper, el patró professional de proveïdor + hook d'accés aplicat a l'usuari actual i al tema visual, com imbricar i sobreescriure proveïdors, i —molt important— quan el context és l'eina equivocada.

Un avís d'abast abans de començar: aquí estudiem el mecanisme del context i l'apliquem a dos casos concrets. El context com a estratègia de gestió de l'estat de tota l'aplicació —el patró complet, quan escala, quan no, el rendiment i la divisió en diversos contextos— és el tema de 07-02, al mòdul dedicat a la gestió de l'estat. No esgotis aquí aquest debat: primer cal dominar l'eina.

Contingut

  1. El problema: l'usuari actual travessant quatre nivells
  2. Què és el context i què no és
  3. Les tres peces del mecanisme
  4. createContext i el valor per defecte
  5. El proveïdor a React 19
  6. useContext i la cerca del proveïdor més proper
  7. El patró professional: proveïdor + hook d'accés
  8. ContextUsuari complet per a CicloUrbano
  9. ContextTema i el canvi d'aspecte
  10. Imbricar i sobreescriure proveïdors
  11. Quan usar context i quan no
  12. L'advertiment de rendiment

  1. El problema: l'usuari actual travessant quatre nivells

CicloUrbano necessita mostrar a la capçalera qui ha iniciat sessió, amb un menú desplegable. L'usuari viu a App, i el menú és quatre nivells més avall:

// src/App.jsx
function App() {
  const [usuari, setUsuari] = useState(usuaris[0]);   // usr-01, Ana Ribera
  const [tema, setTema] = useState('clar');

  return (
    <Disseny usuari={usuari} tema={tema} alCanviarTema={setTema}>
      {/* … */}
    </Disseny>
  );
}

// src/components/Disseny.jsx — NO utilitza cap de les tres props
function Disseny({ usuari, tema, alCanviarTema, children }) {
  return (
    <div className={estils.disseny}>
      <Capcalera usuari={usuari} tema={tema} alCanviarTema={alCanviarTema} />
      <main className={estils.principal}>{children}</main>
      <PeuDePagina />
    </div>
  );
}

// src/components/Capcalera.jsx — TAMPOC les utilitza
function Capcalera({ usuari, tema, alCanviarTema }) {
  return (
    <header className={estils.capcalera}>
      <h1>CicloUrbano</h1>
      <nav>
        <a href="#catalogo">Catàleg</a>
        <a href="#estaciones">Estacions</a>
        <a href="#reservas">Les meves reserves</a>
      </nav>
      <MenuUsuari usuari={usuari} tema={tema} alCanviarTema={alCanviarTema} />
    </header>
  );
}

// src/components/MenuUsuari.jsx — per fi, aquí sí que s'utilitzen
function MenuUsuari({ usuari, tema, alCanviarTema }) {
  return (
    <div className={estils.menu}>
      <span>{usuari.nom}</span>
      {usuari.rol === 'operario' && <a href="#taller">Panell de taller</a>}
      <button type="button" onClick={() => alCanviarTema(tema === 'clar' ? 'fosc' : 'clar')}>
        Tema {tema === 'clar' ? 'fosc' : 'clar'}
      </button>
    </div>
  );
}

Vist a l'arbre:

flowchart TD
    APP["App<br/><b>estat: usuari, tema</b>"] -->|"usuari, tema, alCanviarTema"| DIS["Disseny<br/>❌ no els utilitza"]
    DIS -->|"usuari, tema, alCanviarTema"| CAB["Capcalera<br/>❌ no els utilitza"]
    DIS --> MAIN["main / children"]
    CAB -->|"usuari, tema, alCanviarTema"| MEN["MenuUsuari<br/>✅ AQUÍ s'utilitzen"]
    CAB --> NAV["nav"]
    style DIS fill:#fde68a
    style CAB fill:#fde68a
    style MEN fill:#dcfce7

Els dos components grocs són canonades, no components. I el cost és real:

  • Signatures contaminades. Disseny declara tres props que no li importen. Qui llegeixi el seu codi haurà de seguir el rastre per entendre de què van.
  • Canvis en cascada. Si MenuUsuari necessita demà l'idioma, cal tocar App, Disseny, Capcalera i MenuUsuari. Quatre fitxers per a una dada.
  • Reutilització trencada. No pots usar Disseny en una pantalla que no tingui usuari sense inventar-te un valor.
  • Soroll a les proves. Provar Capcalera obliga a fabricar un usuari fals encara que la prova no vagi d'això.

  1. Què és el context i què no és

El context és un mecanisme perquè un component posi un valor a disposició de tot el seu subarbre de descendents, de manera que qualsevol d'ells el pugui llegir directament, sense rebre'l per props.

També és útil enunciar el que no és, perquè es malinterpreta amb freqüència:

  • No és un magatzem d'estat. El context transporta un valor; qui el desa continua sent useState o useReducer en algun component.
  • No substitueix les props. Les props continuen sent la forma normal de passar dades. El context és l'excepció per al que és «ambiental».
  • No és un canal global. Només arriba als descendents del proveïdor. Un component fora d'aquesta branca no veu res.
  • No trenca el flux unidireccional. La dada continua baixant d'avantpassat a descendent; l'única cosa que desapareix són les escales intermèdies.

La imatge mental útil: si les props són un paquet que va de mà en mà per una cadena de persones, el context és una megafonia. Qui parla és el proveïdor; qui vulgui escoltar, escolta; qui sigui fora de la sala, no sent res.

  1. Les tres peces del mecanisme

Sempre són les mateixes tres, en el mateix ordre:

flowchart LR
    A["1. createContext(perDefecte)<br/><i>crea el canal</i>"] --> B["2. &lt;Context value={x}&gt;<br/><i>emet el valor</i>"]
    B --> C["3. useContext(Context)<br/><i>el llegeix, a qualsevol profunditat</i>"]
    style A fill:#e0f2fe
    style B fill:#fde68a
    style C fill:#dcfce7
Peça On viu Què fa
createContext(valorPerDefecte) En un mòdul a part, fora de components Crea l'objecte de context. S'importa on calgui
El proveïdor Dalt del subarbre que ha de veure el valor Emet el valor per a tots els seus descendents
useContext(Context) En qualsevol component descendent Llegeix el valor del proveïdor més proper

  1. createContext i el valor per defecte

// src/contextos/ContextTema.js
import { createContext } from 'react';

export const ContextTema = createContext('clar');

createContext es crida una sola vegada per context, en l'àmbit del mòdul. Mai dins d'un component: es recrearia a cada render i tots els consumidors perdrien la connexió.

L'argument és el valor per defecte, i la seva regla és contraintuïtiva: només s'utilitza quan un component crida useContext i no troba cap proveïdor per sobre. Si hi ha proveïdor, el valor per defecte és irrellevant, encara que el proveïdor emeti undefined.

Tens dues estratègies, i totes dues són legítimes:

// Estratègia A: un valor per defecte ÚTIL. El component funciona encara que falti el proveïdor.
export const ContextTema = createContext('clar');

// Estratègia B: un valor per defecte IMPOSSIBLE. Serveix per detectar la fallada.
export const ContextUsuari = createContext(null);

L'estratègia A encaixa amb dades que tenen un valor raonable per omissió (un tema visual, un idioma). La B encaixa amb dades l'absència de les quals sempre és un error de muntatge (l'usuari autenticat, un client d'API): hi poses null i ho comproves al hook d'accés, com veuràs a l'apartat 7.

  1. El proveïdor a React 19

import { ContextTema } from './contextos/ContextTema.js';

<ContextTema value={tema}>
  {/* tot el que pengi d'aquí pot llegir el tema */}
</ContextTema>

A React 19 l'objecte de context s'utilitza directament com a component. Abans calia escriure <ContextTema.Provider>, i ho veuràs en pràcticament tot el codi existent i a la documentació de biblioteques:

// Forma anterior: continua funcionant a React 19, marcada com a obsoleta
<ContextTema.Provider value={tema}>
  …
</ContextTema.Provider>
React 18 React 19
Proveir un valor <Context.Provider value={x}> <Context value={x}>
Consumir amb hook useContext(Context) useContext(Context) (igual)
Consumir sense hook <Context.Consumer>{(v) => …}</Context.Consumer> Obsolet: usa el hook

La prop es diu sempre value, estigui la resta del projecte en català o no: és part de l'API de React, com children o key.

Dues precisions sobre el proveïdor:

  • El seu abast és el seu subarbre de JSX, no el seu fitxer ni el seu mòdul. El que no estigui dins dels seus children no veu el valor.
  • El valor pot ser qualsevol cosa: una cadena, un objecte, una funció, o un objecte amb dades i funcions alhora. És el normal quan el subarbre també ha de poder canviar el valor.

  1. useContext i la cerca del proveïdor més proper

import { useContext } from 'react';
import { ContextTema } from '../contextos/ContextTema.js';

function MenuUsuari() {
  const tema = useContext(ContextTema);
  …
}

useContext rep l'objecte de context, no el proveïdor ni el valor. I fa exactament això: puja per l'arbre de components des d'on se'l crida, buscant el primer proveïdor d'aquest mateix context.

flowchart TD
    APP["App"] --> PROV["&lt;ContextTema value='fosc'&gt;"]
    PROV --> DIS["Disseny"]
    DIS --> CAB["Capcalera"]
    CAB --> MEN["MenuUsuari<br/>useContext(ContextTema)"]
    MEN -. "cerca cap amunt" .-> CAB
    CAB -. " " .-> DIS
    DIS -. "trobat!" .-> PROV
    PROV -. "retorna 'fosc'" .-> MEN
    style PROV fill:#fde68a
    style MEN fill:#dcfce7

Punts importants del comportament:

  • La cerca és cap amunt, mai cap als costats ni cap avall. Un component germà del proveïdor no veu res.
  • Guanya el proveïdor més proper. Si n'hi ha dos imbricats, el de dins tapa el de fora (apartat 10).
  • Si no n'hi ha cap, es retorna el valor per defecte de createContext. Aquest és el cas perillós: no hi ha cap avís, cap error, cap advertència a la consola. El component simplement funciona amb dades incorrectes.

Aquest últim punt és la raó de ser del patró de l'apartat següent.

  1. El patró professional: proveïdor + hook d'accés

Usar createContext i useContext solts per tota l'aplicació té tres inconvenients: cada component ha d'importar el context, ningú detecta la falta de proveïdor, i la lògica d'estat acaba repartida. El patró estàndard de l'ecosistema agrupa tot en un mòdul amb tres exportacions: el context (de vegades privat), un component proveïdor i un hook d'accés.

// Esquelet del patró
const Context = createContext(null);            // 1. el canal (pot no exportar-se)

export function ProveidorX({ children }) {       // 2. el proveïdor amb l'estat a dins
  const [valor, setValor] = useState(inicial);
  return <Context value={{ valor, setValor }}>{children}</Context>;
}

export function useX() {                          // 3. el hook d'accés amb validació
  const context = useContext(Context);
  if (context === null) {
    throw new Error('useX s\'ha d\'utilitzar dins de <ProveidorX>');
  }
  return context;
}

Els tres avantatges, que compensen de sobres les deu línies extra:

  • L'error de muntatge es detecta a l'instant, amb un missatge que diu exactament què falta i on. Sense això, oblidar el proveïdor produeix un Cannot read properties of null a deu components de distància.
  • Els consumidors no importen el context, només el hook. Si demà el context es divideix en dos per rendiment (07-02), els consumidors no se n'assabenten.
  • L'estat viu al costat del seu proveïdor, no dispers a App.

  1. ContextUsuari complet per a CicloUrbano

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

// null com a valor per defecte: l'absència de proveïdor SEMPRE és un error
const ContextUsuari = createContext(null);

/**
 * Proveïdor de l'usuari que ha iniciat sessió a CicloUrbano.
 * Props:
 *  - children (contingut de l'aplicació)
 *  - usuariInicial (objecte Usuari, opcional, per defecte usr-01)
 */
export function ProveidorUsuari({ children, usuariInicial = usuaris[0] }) {
  const [usuari, setUsuari] = useState(usuariInicial);

  function iniciarSessio(id) {
    const trobat = usuaris.find((candidat) => candidat.id === id);
    if (trobat) setUsuari(trobat);
  }

  function tancarSessio() {
    setUsuari(null);
  }

  const valor = {
    usuari,
    esOperari: usuari?.rol === 'operario',
    iniciarSessio,
    tancarSessio
  };

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

/**
 * Accés a l'usuari actual. Llança si s'usa fora del proveïdor.
 */
export function useUsuari() {
  const context = useContext(ContextUsuari);
  if (context === null) {
    throw new Error('useUsuari s\'ha d\'utilitzar dins de <ProveidorUsuari>');
  }
  return context;
}

Detalls del disseny que convé justificar:

  • El fitxer és .jsx, no .js, perquè conté JSX. I no porta ñ ni accents en el nom, seguint la convenció del projecte.
  • ContextUsuari no s'exporta. Ningú de fora el necessita: el proveïdor l'utilitza i el hook el llegeix. Així és impossible saltar-se la validació.
  • esOperari és un valor derivat que es calcula al proveïdor. Evita repetir usuari.rol === 'operario' a cada consumidor, amb el risc d'escriure-ho malament.
  • Les funcions viatgen dins del valor. El context no només porta dades: porta també la manera de canviar-les, que és el que evita continuar passant alCanviarUsuari per props.

Ara MenuUsuari es llegeix sense buscar res:

// src/components/MenuUsuari.jsx
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import estils from './MenuUsuari.module.css';

function MenuUsuari() {
  const { usuari, esOperari, tancarSessio } = useUsuari();

  if (!usuari) {
    return <a href="#acceso" className={estils.acces}>Iniciar sessió</a>;
  }

  return (
    <div className={estils.menu}>
      <span className={estils.nom}>{usuari.nom}</span>
      {esOperari && <a href="#taller">Panell de taller</a>}
      <button type="button" onClick={tancarSessio}>Tancar sessió</button>
    </div>
  );
}

export default MenuUsuari;

I Disseny i Capcalera tornen a ser el que eren abans de la perforació:

// src/components/Disseny.jsx — sense ni una sola prop de més
function Disseny({ children }) {
  return (
    <div className={estils.disseny}>
      <Capcalera />
      <main className={estils.principal}>{children}</main>
      <PeuDePagina />
    </div>
  );
}

L'arbre, després:

flowchart TD
    APP["App"] --> PU["&lt;ProveidorUsuari&gt;<br/><b>estat: usuari</b>"]
    PU --> DIS["Disseny<br/>✅ sense props"]
    DIS --> CAB["Capcalera<br/>✅ sense props"]
    CAB --> MEN["MenuUsuari<br/>useUsuari()"]
    PU -. "canal directe" .-> MEN
    style PU fill:#fde68a
    style DIS fill:#dcfce7
    style CAB fill:#dcfce7
    style MEN fill:#dcfce7

I així queda main.jsx, amb el proveïdor per sobre d'App i el límit d'error global de 04-05 per sobre de tot:

// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import LimitError from './components/LimitError.jsx';
import { ProveidorUsuari } from './contextos/ContextUsuari.jsx';
import { ProveidorTema } from './contextos/ContextTema.jsx';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <LimitError titol="CicloUrbano no està disponible">
      <ProveidorTema>
        <ProveidorUsuari>
          <App />
        </ProveidorUsuari>
      </ProveidorTema>
    </LimitError>
  </StrictMode>
);

  1. ContextTema i el canvi d'aspecte

El segon cas canònic. El tema visual el llegeix mitja aplicació i el canvia un únic botó: la definició exacta de dada ambiental.

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

const ContextTema = createContext(null);

const TEMES = ['clar', 'fosc'];

/**
 * Proveïdor del tema visual de CicloUrbano.
 * Props:
 *  - children (contingut)
 *  - temaInicial (cadena, opcional, 'clar' o 'fosc')
 */
export function ProveidorTema({ children, temaInicial = 'clar' }) {
  const [tema, setTema] = useState(() => {
    const desat = localStorage.getItem('ciclourbano:tema');
    return TEMES.includes(desat) ? desat : temaInicial;
  });

  // Sincronitza l'atribut de l'<html> i l'emmagatzematge del navegador (05-02)
  useEffect(() => {
    document.documentElement.dataset.tema = tema;
    localStorage.setItem('ciclourbano:tema', tema);
  }, [tema]);

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

  return (
    <ContextTema value={{ tema, esFosc: tema === 'fosc', alternarTema }}>
      {children}
    </ContextTema>
  );
}

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

L'estat inicial utilitza inicialització mandrosa (05-01) per no llegir localStorage a cada render, i l'efecte sincronitza amb dos sistemes externs (05-02): l'atribut data-tema de l'<html> i l'emmagatzematge del navegador. Les variables CSS de index.css responen a aquest atribut:

/* src/index.css (fragment) */
:root {
  --color-marca: #12805c;
  --color-fons: #f5f7fa;
  --color-superficie: #ffffff;
  --color-text: #1f2933;
  --color-vora: #d9e2ec;
}

:root[data-tema='fosc'] {
  --color-fons: #111a22;
  --color-superficie: #1c2733;
  --color-text: #e6edf3;
  --color-vora: #2d3b48;
}

Fixa't en el repartiment de responsabilitats: React gestiona una dada ('clar' o 'fosc') i el CSS fa tota la feina visual mitjançant variables. Cap component necessita useTema per pintar-se diferent; només el necessita qui hagi de decidir alguna cosa en funció del tema, com el botó que l'alterna:

// src/components/BotoTema.jsx
import { useTema } from '../contextos/ContextTema.jsx';

function BotoTema() {
  const { esFosc, alternarTema } = useTema();

  return (
    <button type="button" onClick={alternarTema} aria-pressed={esFosc}>
      <span aria-hidden="true">{esFosc ? '☀' : '☾'}</span>
      {esFosc ? 'Tema clar' : 'Tema fosc'}
    </button>
  );
}

export default BotoTema;

L'aria-pressed ve de 03-06 i de la convenció ja establerta a SelectorTipus: un botó que representa un estat activable ha d'anunciar-ho.

  1. Imbricar i sobreescriure proveïdors

Un mateix context pot tenir diversos proveïdors en branques diferents, o fins i tot imbricats. Guanya sempre el més proper cap amunt.

function App() {
  return (
    <ProveidorTema temaInicial="clar">
      <Disseny>
        <PanellCataleg />          {/* llegeix 'clar' */}

        {/* El panell de taller sempre es mostra en fosc, sense tocar el global */}
        <ProveidorTema temaInicial="fosc">
          <PanellTaller />          {/* llegeix 'fosc' */}
        </ProveidorTema>
      </Disseny>
    </ProveidorTema>
  );
}
flowchart TD
    PT1["&lt;ProveidorTema 'clar'&gt;"] --> DIS["Disseny"]
    DIS --> CAT["PanellCataleg<br/>useTema() → clar"]
    DIS --> PT2["&lt;ProveidorTema 'fosc'&gt;"]
    PT2 --> TAL["PanellTaller<br/>useTema() → fosc"]
    TAL --> SUB["Subcomponents<br/>useTema() → fosc"]
    style PT1 fill:#e0f2fe
    style PT2 fill:#334155,color:#ffffff

Casos on això és útil de veritat: una vista prèvia que s'ha de mostrar amb el tema contrari, una secció amb un idioma diferent, o —molt freqüent— les proves (Mòdul 9), on embolcalles el component sota prova en un proveïdor amb valors controlats.

Quan hi ha diversos contextos diferents, s'imbriquen sense més. Si l'escala es torna incòmoda, un component que agrupa tots els proveïdors ho resol:

// src/contextos/Proveidors.jsx
export function Proveidors({ children }) {
  return (
    <ProveidorTema>
      <ProveidorUsuari>
        <ProveidorReserves>{children}</ProveidorReserves>
      </ProveidorUsuari>
    </ProveidorTema>
  );
}

  1. Quan usar context i quan no

El context té un cost que no es veu al codi: fa implícita una dependència que abans era explícita. En llegir <MenuUsuari /> ja no saps d'on treu les seves dades; has d'obrir el fitxer. En un component que s'utilitza en un sol lloc, això és pitjor que una prop.

Situació Eina correcta Per què
El fill directe necessita una dada del pare Props Explícit, traçable, sense cerimònia
Dos germans comparteixen una dada Elevar l'estat (04-01) L'avantpassat comú és a un pas
La dada només travessa un o dos nivells Props El context no compensa
Un component intermedi només passa la dada perquè no hi ha més remei Composició amb children (04-02) Sol eliminar la perforació sense context
Molts components de tot l'arbre llegeixen la dada i pocs la canvien Context És exactament el seu cas d'ús
La dada és «ambiental»: usuari, tema, idioma, permisos, format de moneda Context Ambiental = llegit a tot arreu
Estat del servidor amb memòria cau i revalidació Biblioteques específiques (07-06) El context no fa memòria cau ni revalida

La quarta fila mereix un exemple, perquè molta gent munta un context quan la composició ja n'hi havia prou. Aquest Disseny rep usuari només per donar-lo a la capçalera:

// Perforació per falta de composició
<Disseny usuari={usuari}>
  <PanellCataleg />
</Disseny>

Amb un forat amb nom (04-02), la dada ja no travessa res:

// src/components/Disseny.jsx
function Disseny({ capcalera, children }) {
  return (
    <div className={estils.disseny}>
      {capcalera}
      <main className={estils.principal}>{children}</main>
      <PeuDePagina />
    </div>
  );
}

// Ús: App crea la capçalera amb l'usuari i l'entrega ja muntada
<Disseny capcalera={<Capcalera usuari={usuari} />}>
  <PanellCataleg />
</Disseny>

Disseny ja no sap res de l'usuari: rep un element ja construït. Abans de muntar un context, comprova si la composició resol el cas; és més senzill i manté les dependències visibles.

La regla que resumeix l'apartat: el context és per a dades ambientals que molts llegeixen i pocs canvien. Quan la dada és específica d'una interacció concreta, les props continuen sent la resposta.

  1. L'advertiment de rendiment

Una frase, i la desenvoluparem al seu lloc: quan canvia el valor d'un context, tots els components que el consumeixen es tornen a renderitzar, encara que només facin servir una part del valor i aquesta part no hagi canviat. Amb un tema que s'alterna dues vegades al dia és irrellevant; amb un valor que canvia a cada pulsació de tecla, importa.

Les tècniques per gestionar-ho —dividir un context en diversos segons la seva freqüència de canvi, separar les dades de les funcions que les modifiquen, i memoïtzar el valor del proveïdor— pertanyen a 07-02 i al Mòdul 8. Aquí queda't amb la idea que existeix el cost, i amb el costum de no ficar en un mateix context coses que canvien a ritmes molt diferents.

Errors Comuns i Consells

  • Cridar createContext dins d'un component. Es crea un context nou a cada render i els consumidors deixen de trobar el proveïdor. Va sempre a l'àmbit del mòdul.
  • Oblidar el proveïdor. Sense el patró de l'apartat 7 no hi ha error: el component rep el valor per defecte i funciona malament en silenci. Amb el hook d'accés que llança, la fallada apareix al primer render amb un missatge clar.
  • Creure que el valor per defecte s'utilitza quan el proveïdor emet undefined. No: només s'utilitza si no hi ha proveïdor en tota la cadena.
  • Proveir des del component equivocat. El proveïdor ha d'estar per sobre de tots els consumidors. Si el poses dins de Capcalera, el catàleg no el veurà.
  • Ficar tot l'estat de l'aplicació en un únic context. Qualsevol canvi repinta tots els consumidors. Un context per assumpte.
  • Usar context per passar una dada a un fill directe. És complicar una prop. El context comença a compensar a partir de tres nivells, i només si hi ha diversos consumidors.
  • Exportar el context i el hook alhora sense necessitat. Exportar només el proveïdor i el hook impedeix que algú es salti la validació.
  • Consell: anomena el hook amb use + el substantiu (useUsuari, useTema). A més de la convenció, és obligatori perquè les regles del linter el tractin com a hook (04-04).
  • Consell: a les proves, embolcalla el component en el seu proveïdor amb valors fixos. Si et resulta difícil, sol ser senyal que el context té massa responsabilitats.
  • Consell: posa al valor del context els derivats que farien servir diversos consumidors (esOperari), no només les dades crues. Evita repetir la mateixa condició per tot arreu.

Exercicis

Exercici 1. Aquest codi de CicloUrbano falla en temps d'execució amb «Cannot destructure property 'usuari' of null». Troba les dues causes i corregeix-les.

// src/App.jsx
import { ProveidorUsuari, useUsuari } from './contextos/ContextUsuari.jsx';

function App() {
  const { usuari } = useUsuari();

  return (
    <ProveidorUsuari>
      <Disseny>
        <p>Benvinguda, {usuari.nom}</p>
        <PanellCataleg />
      </Disseny>
    </ProveidorUsuari>
  );
}

Exercici 2. Crea ContextAvisos amb el patró proveïdor + hook d'accés. Ha de permetre que qualsevol component de l'arbre mostri un avís global sense rebre props: el proveïdor desa una llista d'avisos { id, to, text } (els tons són els d'Avis: info, exit, advertencia, error), exposa mostrarAvis(to, text) i descartarAvis(id), i pinta la pila d'avisos per sobre de children. Mostra a més com ho faria servir FormulariReserva en crear una reserva.

Exercici 3. TargetaBicicleta ha de mostrar un botó «Enviar al taller» només si l'usuari actual és operari (usr-02, Marc Solé). Avui rep usuari per props des d'App, travessant LlistaBicicletes. Reescriu-lo amb useUsuari i explica quines props desapareixen de cada component de la cadena.

Solucions

Solució 1.

Les dues causes:

  1. App crida useUsuari() però és el mateix App qui renderitza <ProveidorUsuari>. Un component no pot consumir un context que ell mateix proveeix: la cerca de useContext va cap amunt, i el proveïdor és per sota de la línia on es crida el hook. useContext retorna null (el valor per defecte) i la desestructuració rebenta.
  2. usuari.nom es llegeix sense comprovar que hi ha usuari. El proveïdor permet tancarSessio(), que posa usuari a null; aquest <p> fallaria tan bon punt algú tanqués sessió.

La correcció mou el proveïdor per sobre d'App (a main.jsx) i extreu la salutació a un component descendent:

// src/main.jsx
createRoot(document.getElementById('root')).render(
  <StrictMode>
    <ProveidorUsuari>
      <App />
    </ProveidorUsuari>
  </StrictMode>
);

// src/App.jsx — ja no crida el hook: només compon
function App() {
  return (
    <Disseny>
      <Benvinguda />
      <PanellCataleg />
    </Disseny>
  );
}

// src/components/Benvinguda.jsx — descendent del proveïdor: aquí sí
import { useUsuari } from '../contextos/ContextUsuari.jsx';

function Benvinguda() {
  const { usuari } = useUsuari();

  if (!usuari) return <p>Benvingut a CicloUrbano. Inicia sessió per reservar.</p>;
  return <p>Benvinguda, {usuari.nom}</p>;
}

Solució 2.

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

const ContextAvisos = createContext(null);

/**
 * Proveïdor de la pila d'avisos globals de CicloUrbano.
 * Props:
 *  - children (contingut de l'aplicació)
 */
export function ProveidorAvisos({ children }) {
  const [avisos, setAvisos] = useState([]);

  function mostrarAvis(to, text) {
    const id = `avi-${crypto.randomUUID().slice(0, 8)}`;
    setAvisos((previs) => [...previs, { id, to, text }]);
    return id;
  }

  function descartarAvis(id) {
    setAvisos((previs) => previs.filter((avis) => avis.id !== id));
  }

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

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

Ús des de FormulariReserva, sense ni una sola prop nova a la cadena:

// src/components/FormulariReserva.jsx (fragment)
import { useAvisos } from '../contextos/ContextAvisos.jsx';

function FormulariReserva({ alCrearReserva, usuariId = 'usr-01' }) {
  const { mostrarAvis } = useAvisos();

  function gestionarEnvio(esdeveniment) {
    esdeveniment.preventDefault();
    if (Object.keys(errors).length > 0) {
      mostrarAvis('error', 'Revisa els camps marcats abans de continuar.');
      return;
    }
    const reserva = {
      id: `res-${crypto.randomUUID().slice(0, 8)}`,
      bicicletaId: dades.bicicletaId,
      usuari: usuariId,
      dataInici: dades.dataInici,
      hores: dades.hores,
      estat: 'activa'
    };
    alCrearReserva(reserva);
    mostrarAvis('exit', `Reserva ${reserva.id} creada per ${reserva.hores} hores.`);
    setDades(DADES_INICIALS);
  }
  …
}

Aquest és el cas d'ús ideal del context: els avisos són ambientals (qualsevol component els pot llançar), es pinten en un únic lloc i ningú ha d'assabentar-se de com arriben. Sense context, mostrarAvis s'hauria de passar com a prop des d'App a tots els formularis i panells de l'aplicació. Fixa't també en el role="status" amb aria-live="polite" de 03-06: els avisos apareixen sense moure el focus, així que cal anunciar-los.

Solució 3.

// src/components/TargetaBicicleta.jsx
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import EtiquetaEstat from './EtiquetaEstat.jsx';
import estils from './TargetaBicicleta.module.css';

/**
 * Props:
 *  - bicicleta       (objecte Bicicleta, obligatori)
 *  - nomEstacio      (cadena, opcional)
 *  - alSeleccionar   (funció, opcional)
 *  - alReservar      (funció, opcional)
 *  - alEnviarATaller (funció, opcional): només s'utilitza si l'usuari és operari
 */
function TargetaBicicleta({ bicicleta, nomEstacio, alSeleccionar, alReservar, alEnviarATaller }) {
  const { esOperari } = useUsuari();
  const preuFormatat = bicicleta.preuHora.toFixed(2).replace('.', ',');

  return (
    <article className={estils.targeta}>
      <h3>{bicicleta.model}</h3>
      <EtiquetaEstat estat={bicicleta.estat} />
      <p>{nomEstacio} · {preuFormatat} €/h</p>

      <button type="button" onClick={() => alSeleccionar?.(bicicleta)}>Veure detalls</button>
      <button
        type="button"
        onClick={() => alReservar?.(bicicleta)}
        disabled={bicicleta.estat !== 'disponible'}
      >
        Reservar
      </button>

      {esOperari && bicicleta.estat !== 'mantenimiento' && (
        <button type="button" onClick={() => alEnviarATaller?.(bicicleta)}>
          Enviar al taller
        </button>
      )}
    </article>
  );
}

export default TargetaBicicleta;

Props que desapareixen de la cadena:

Component Abans Després
App Passava usuari a LlistaBicicletes Res: el proveïdor és a main.jsx
LlistaBicicletes Rebia usuari i el reenviava sense usar-lo Torna al seu contracte: bicicletes, estacions, alSeleccionar, alReservar
TargetaBicicleta Rebia usuari com a prop El llegeix amb useUsuari()

Fixa't en el que no ha canviat: alEnviarATaller continua sent una prop normal. L'usuari és una dada ambiental, però «què fer quan es prem aquest botó concret d'aquesta targeta concreta» és una decisió del pare, i això és territori de props. Confondre les dues coses —ficar-ho tot al context perquè és còmode— és l'error que converteix una aplicació en un cabdell.

Conclusió

La perforació de props que 04-01 va deixar pendent té nom i solució. El context és un canal directe entre un avantpassat i tots els seus descendents, muntat sempre amb les mateixes tres peces: createContext(valorPerDefecte) en un mòdul a part, un proveïdor que a React 19 s'escriu <Context value={…}> —amb <Context.Provider> encara funcionant en el codi que heretis— i useContext(Context), que puja per l'arbre fins al proveïdor més proper i, si no en troba cap, retorna el valor per defecte sense avisar de res. Per això el patró professional embolcalla les tres peces en un mòdul amb proveïdor + hook d'accés que llança un error explícit quan falta el proveïdor: així ho has construït a ContextUsuari i ContextTema, amb Disseny i Capcalera recuperant les seves signatures netes. Saps imbricar proveïdors per sobreescriure el valor en una branca, i —el més important— saps quan no usar-lo: per a fills directes hi ha les props, per a germans hi ha elevar l'estat, i per a canonades hi ha la composició amb children. El context és per al que és ambiental: usuari, tema, idioma, permisos, avisos.

Queda un front obert. ProveidorUsuari gestionava una dada simple, però l'estat de reserves de CicloUrbano no ho és: hi ha una llista de reserves, un esborrany en curs, una fase d'enviament i un possible error, i totes aquestes peces canvien alhora i amb regles. Amb useState acabaries amb cinc variables soltes i gestors que en toquen tres alhora, justament la situació que 05-01 va anunciar com a límit de l'eina. React ofereix una alternativa que reuneix totes les transicions en una única funció pura, fàcil de llegir, de provar i de raonar. La següent lliçó és Hook useReducer.

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