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
- El problema: l'usuari actual travessant quatre nivells
- Què és el context i què no és
- Les tres peces del mecanisme
createContexti el valor per defecte- El proveïdor a React 19
useContexti la cerca del proveïdor més proper- El patró professional: proveïdor + hook d'accés
ContextUsuaricomplet per a CicloUrbanoContextTemai el canvi d'aspecte- Imbricar i sobreescriure proveïdors
- Quan usar context i quan no
- L'advertiment de rendiment
- 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.
Dissenydeclara 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
MenuUsuarinecessita demà l'idioma, cal tocarApp,Disseny,CapcaleraiMenuUsuari. Quatre fitxers per a una dada. - Reutilització trencada. No pots usar
Dissenyen una pantalla que no tingui usuari sense inventar-te un valor. - Soroll a les proves. Provar
Capcaleraobliga a fabricar un usuari fals encara que la prova no vagi d'això.
- 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
useStateouseReduceren 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.
- 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. <Context value={x}><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 |
createContext i el valor per defecte
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.
- 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
childrenno 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.
useContext i la cerca del proveïdor més proper
useContext i la cerca del proveïdor més properimport { 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["<ContextTema value='fosc'>"]
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.
- 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 nulla 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.
ContextUsuari complet per a CicloUrbano
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. ContextUsuarino 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 repetirusuari.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
alCanviarUsuariper 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["<ProveidorUsuari><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>
);
ContextTema i el canvi d'aspecte
ContextTema i el canvi d'aspecteEl 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.
- 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["<ProveidorTema 'clar'>"] --> DIS["Disseny"]
DIS --> CAT["PanellCataleg<br/>useTema() → clar"]
DIS --> PT2["<ProveidorTema 'fosc'>"]
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>
);
}
- 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:
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.
- 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
createContextdins 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:
AppcridauseUsuari()però és el mateixAppqui renderitza<ProveidorUsuari>. Un component no pot consumir un context que ell mateix proveeix: la cerca deuseContextva cap amunt, i el proveïdor és per sota de la línia on es crida el hook.useContextretornanull(el valor per defecte) i la desestructuració rebenta.usuari.nomes llegeix sense comprovar que hi ha usuari. El proveïdor permettancarSessio(), que posausuarianull; 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
- Què és React?
- Configuració de l'Entorn de Desenvolupament
- Hola Món amb React
- JSX: Extensió de Sintaxi de JavaScript
- Com Renderitza React: Virtual DOM i Reconciliació
Mòdul 2: Components de React
- Entendre els Components
- Components Funcionals vs de Classe
- Props: Passar Dades als Components
- State: Gestió de l'Estat del Component
- Estils en els Components: CSS, Mòduls i Utilitats
Mòdul 3: Treballar amb Esdeveniments
- Gestió d'Esdeveniments a React
- Renderitzat Condicional
- Llistes i Claus
- Formularis i Components Controlats
- Validació de Formularis i Components No Controlats
- Accessibilitat en Components Interactius
Mòdul 4: Conceptes Avançats de Components
- Elevar l'Estat
- Composició vs Herència
- Mètodes del Cicle de Vida de React
- Hooks: Introducció i Ús Bàsic
- Límits d'Error: Capturar Fallades a la Interfície
Mòdul 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef i Accés al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalitzats
Mòdul 6: Enrutament a React
- Introducció a React Router
- Configuració de React Router
- Rutes Imbricades
- Navegació Programàtica
- Rutes Protegides i Control d'Accés
Mòdul 7: Gestió de l'Estat
- Introducció a la Gestió de l'Estat
- API de Context
- Redux: Introducció i Configuració
- Redux: Accions i Reductors
- Redux: Connectar-lo a React
- Estat del Servidor: Peticions, Memòria Cau i Sincronització
Mòdul 8: Optimització del Rendiment
- Tècniques d'Optimització del Rendiment a React
- Memoïtzació amb React.memo
- Hooks useMemo i useCallback
- Divisió de Codi i Càrrega Mandrosa
- Mesurar el Rendiment amb React DevTools Profiler
Mòdul 9: Proves a React
- Introducció a les Proves
- Proves Unitàries amb Jest
- Proves de Components amb React Testing Library
- Proves de Codi Asíncron i Simulació d'APIs
- Proves d'Extrem a Extrem amb Cypress
Mòdul 10: Temes Avançats
- Renderitzat al Servidor (SSR) amb Next.js
- Generació de Llocs Estàtics (SSG) amb Next.js
- Suspense i React Server Components
- TypeScript amb React
- React Native: Creació d'Aplicacions Mòbils
