Amb el catàleg de CicloUrbano ja filtrant de debò, App comença a créixer: seccions per embolicar, avisos per mostrar, panells que es repeteixen amb petites variacions. Qui ve de Java, C# o PHP té aquí un reflex automàtic: crear una classe base PanellBase i fer que PanellCataleg i PanellReserves n'heretin. A React aquest reflex és l'equivocat. La documentació oficial és explícita: no s'ha trobat cap cas d'ús en què l'herència de components sigui preferible a la composició. En aquesta lliçó aprendràs per què, i què s'usa en el seu lloc: children, espais amb nom, especialització per configuració, render props i components d'ordre superior, amb una regla final que ordena totes les decisions de reutilització que prendràs a React.
Contingut
- Per què React no fa servir herència de components
- Composició amb
children: contenidors genèrics - Espais amb nom: diverses props que reben JSX
- Especialització per configuració: de
AvisaAvisManteniment childrencom a funció: les render props- Components d'ordre superior (HOC): com es llegeixen
- Taula comparativa de les cinc tècniques
- La regla pràctica: composar interfície, extreure lògica
- Exemple central: la pantalla de catàleg recomposta
- Per què React no fa servir herència de components
L'herència clàssica resol la reutilització dient «B és un A i a més fa això altre». Funciona bé amb jerarquies de dades estables, i molt malament amb interfícies, per tres raons concretes.
Primera: les interfícies no formen jerarquies netes. Un panell de reserves és un panell? I si a més necessita el comportament d'un modal i el d'una llista filtrable? Amb herència simple hauries d'escollir-ne un; amb jerarquies profundes acabes amb PanellBaseAmbCapcaleraIAccions, un nom que confessa el problema.
Segona: l'herència acobla per dins. La subclasse depèn de detalls interns de la classe pare. Canviar el pare trenca fills que ni tan sols has obert. En una base de codi amb dos-cents components això és inmanejable.
Tercera, i decisiva: React ja té una relació de contenció millor. Un component no necessita ser un altre per reutilitzar-lo: n'hi ha prou amb usar-lo al seu JSX. Això és composició, i produeix una relació «té un» en lloc de «és un».
// HERÈNCIA (no ho facis a React)
class PanellReserves extends Panell { // "PanellReserves ÉS UN Panell"
render() { /* … i ara, com reutilitzo el que pintava Panell? */ }
}
// COMPOSICIÓ (l'idiomàtic)
function PanellReserves({ reserves }) { // "PanellReserves TÉ UN Panell"
return (
<Panell titol="Les meves reserves">
<LlistaReserves reserves={reserves} />
</Panell>
);
}I hi ha un argument tècnic que tanca la discussió: els components de funció no es poden estendre. No hi ha classe, no hi ha extends, no hi ha super. Des que els hooks es van convertir en la forma estàndard d'escriure React, l'herència de components va deixar de ser fins i tot una opció disponible. Les classes sobreviuen com a codi heretat (04-03) i per als límits d'error (04-05), però fins i tot allà s'estén Component, la classe de React, mai un component teu.
- Composició amb
children: contenidors genèrics
children: contenidors genèricschildren és la prop especial que ja coneixes des de 02-03: conté tot el que s'escriu entre l'etiqueta d'obertura i la de tancament d'un component. És l'eina de composició número u, i serveix per escriure contenidors genèrics: components que aporten estructura i estil sense saber què contindran.
El Panell de CicloUrbano és exactament això:
// src/components/Panell.jsx
import estils from './Panell.module.css';
/**
* Contenidor amb títol i contingut lliure.
* Props:
* - titol (cadena, obligatori)
* - children (contingut, obligatori)
* - peu (element JSX, opcional)
*/
function Panell({ titol, children, peu }) {
return (
<section className={estils.panell}>
<h2 className={estils.titol}>{titol}</h2>
<div className={estils.cos}>{children}</div>
{peu && <div className={estils.peu}>{peu}</div>}
</section>
);
}
export default Panell;Panell no sap absolutament res de bicicletes, i per això serveix per a tot:
<Panell titol="Catàleg" peu={<small>Preus amb IVA inclòs.</small>}>
<LlistaBicicletes bicicletes={bicicletesVisibles} estacions={estacions} />
</Panell>
<Panell titol="Les meves reserves">
<LlistaReserves reserves={reserves} />
</Panell>Aquest és el punt que cal interioritzar: l'espai children és el que l'herència intentava aconseguir amb mètodes abstractes, però sense acoblar res. El contenidor defineix el marc; qui l'usa decideix el contingut.
Dos contenidors més de la mateixa família:
// src/components/Disseny.jsx
import estils from './Disseny.module.css';
/**
* Estructura general d'una pantalla de CicloUrbano.
* Props:
* - children (contingut principal)
*/
function Disseny({ children }) {
return (
<div className={estils.disseny}>
<Capcalera />
<main className={estils.principal}>{children}</main>
<PeuDePagina />
</div>
);
}
export default Disseny;// src/components/Modal.jsx
import estils from './Modal.module.css';
/**
* Finestra modal genèrica.
* Props:
* - titol (cadena, obligatori)
* - children (contingut)
* - alTancar (funció, obligatòria)
*/
function Modal({ titol, children, alTancar }) {
return (
<div className={estils.fons} onClick={alTancar}>
<div
className={estils.finestra}
role="dialog"
aria-modal="true"
aria-label={titol}
onClick={(esdeveniment) => esdeveniment.stopPropagation()}
>
<h2>{titol}</h2>
{children}
<button type="button" onClick={alTancar}>Tancar</button>
</div>
</div>
);
}
export default Modal;El stopPropagation() del segon onClick és el de 03-01: sense ell, un clic dins de la finestra pujaria per bombolla fins al fons i tancaria el modal. Fixa't que Modal tampoc no sap què conté: pot embolicar un PanellReserva, un FormulariReserva o un text de confirmació, sense canviar ni una línia.
Nota sobre el nom del fitxer: a diferència de Capcalera —on calia evitar la ç del nom en el fitxer—, Disseny no porta cap diacrític problemàtic, així que el component i el fitxer comparteixen exactament el mateix nom: Disseny.jsx. La regla de fons és la mateixa que ja vas veure amb Capcalera: el nom d'un fitxer ha de ser segur en qualsevol sistema, sense caràcters especials que puguin donar problemes; simplement, en aquest cas la paraula catalana ja hi compleix per si sola.
- Espais amb nom: diverses props que reben JSX
Un únic children es queda curt tan bon punt el contenidor té diverses zones. La solució no és inventar convencions sobre l'ordre dels fills, sinó acceptar JSX en props normals. A cada prop d'aquest tipus se l'anomena espai amb nom (named slot).
// src/components/PanellAvancat.jsx
import estils from './PanellAvancat.module.css';
/**
* Panell amb quatre espais independents.
* Props:
* - titol (cadena, obligatori)
* - capcalera (JSX, opcional): contingut extra sota el títol
* - accions (JSX, opcional): botons de la cantonada superior dreta
* - children (JSX): el cos
* - peu (JSX, opcional)
*/
function PanellAvancat({ titol, capcalera, accions, children, peu }) {
return (
<section className={estils.panell}>
<header className={estils.capcalera}>
<div>
<h2 className={estils.titol}>{titol}</h2>
{capcalera}
</div>
{accions && <div className={estils.accions}>{accions}</div>}
</header>
<div className={estils.cos}>{children}</div>
{peu && <footer className={estils.peu}>{peu}</footer>}
</section>
);
}
export default PanellAvancat;I el seu ús al catàleg de CicloUrbano:
<PanellAvancat
titol="Catàleg de bicicletes"
capcalera={<p>Mostrant {bicicletesVisibles.length} de {bicicletes.length} bicicletes.</p>}
accions={
<SelectorTipus tipusTriat={tipusTriat} alCanviarTipus={gestionarCanviTipus} />
}
peu={<small>Preus amb IVA inclòs · Tarifa mínima: 1 hora</small>}
>
<LlistaBicicletes
bicicletes={bicicletesVisibles}
estacions={estacions}
alSeleccionar={gestionarSeleccioBicicleta}
alReservar={gestionarReserva}
/>
</PanellAvancat>No hi ha res de màgic: una prop pot contenir JSX igual que conté un número o una cadena, perquè JSX és només un valor de JavaScript. El propi Panell original ja ho feia amb peu.
Quan triar cada opció:
| Situació | Tècnica |
|---|---|
| El contenidor té una sola zona de contingut | children |
| El contenidor té dues o més zones amb significats diferents | Espais amb nom |
| Alguna zona és opcional i s'ha de poder ometre netament | Espais amb nom + {zona && …} |
| L'ordre o el nombre de fills és lliure | children |
| Necessites que la zona sigui reconeixible en llegir el codi de qui l'usa | Espais amb nom (accions={…} s'autoexplica) |
Un avantatge poc evident dels espais amb nom: el JSX que passes es crea al component pare, així que pot llegir l'estat del pare sense problemes. A l'exemple, SelectorTipus rep tipusTriat d'App encara que es pinti dins de PanellAvancat, que no sap res de filtres. La posició a l'arbre i la propietat de les dades són coses diferents.
- Especialització per configuració: de
Avis a AvisManteniment
Avis a AvisMantenimentAquí hi ha el substitut directe de l'herència. Quan vulguis un «cas concret» d'un component genèric, no l'estenguis: escriu un altre component que l'usi amb props fixes.
Primer, el component genèric:
// src/components/Avis.jsx
import { classes } from '../utilitats/classes.js';
import estils from './Avis.module.css';
const SIMBOLS = {
info: 'ℹ',
exit: '✓',
advertencia: '!',
error: '×'
};
/**
* Avís genèric de CicloUrbano.
* Props:
* - to (cadena: 'info' | 'exit' | 'advertencia' | 'error'; per defecte 'info')
* - titol (cadena, opcional)
* - children (contingut de l'avís)
* - accions (JSX, opcional): botons al peu de l'avís
*/
function Avis({ to = 'info', titol, children, accions }) {
const urgent = to === 'error' || to === 'advertencia';
return (
<div
className={classes(estils.avis, estils[to])}
role={urgent ? 'alert' : 'status'}
>
<span className={estils.simbol} aria-hidden="true">{SIMBOLS[to]}</span>
<div className={estils.contingut}>
{titol && <p className={estils.titol}>{titol}</p>}
<div>{children}</div>
{accions && <div className={estils.accions}>{accions}</div>}
</div>
</div>
);
}
export default Avis;Detalls que arrosseguen lliçons anteriors: l'aria-hidden del símbol és el mateix patró que EtiquetaEstat (03-06), perquè un lector de pantalla no ha de llegir «signe d'admiració»; i el role canvia segons la urgència, alert per al que interromp i status per al que informa.
Ara, les especialitzacions. Ni una sola paraula clau d'herència:
// src/components/AvisManteniment.jsx
import Avis from './Avis.jsx';
/**
* Avís específic per a bicicletes al taller.
* Props:
* - bicicleta (objecte, obligatori)
* - alAvisarOperari (funció, opcional)
*/
function AvisManteniment({ bicicleta, alAvisarOperari }) {
return (
<Avis
to="advertencia"
titol="Bicicleta en manteniment"
accions={
alAvisarOperari && (
<button type="button" onClick={() => alAvisarOperari(bicicleta)}>
Avisar a un operari
</button>
)
}
>
La bicicleta <strong>{bicicleta.model}</strong> ({bicicleta.id}) és al taller
i no admet reserves. Consulta el catàleg per veure alternatives disponibles.
</Avis>
);
}
export default AvisManteniment;// src/components/AvisReservaCreada.jsx
import Avis from './Avis.jsx';
/**
* Confirmació d'una reserva acabada de crear.
* Props:
* - reserva (objecte Reserva, obligatori)
* - alTancar (funció, opcional)
*/
function AvisReservaCreada({ reserva, alTancar }) {
const data = new Date(reserva.dataInici).toLocaleString('ca-ES');
return (
<Avis
to="exit"
titol="Reserva confirmada"
accions={
alTancar && (
<button type="button" onClick={alTancar}>Entès</button>
)
}
>
La teva reserva <strong>{reserva.id}</strong> queda registrada per al {data},
amb una durada de {reserva.hores === 1 ? '1 hora' : `${reserva.hores} hores`}.
</Avis>
);
}
export default AvisReservaCreada;Compara els dos models mentals:
| Amb herència | Amb especialització per configuració |
|---|---|
class AvisManteniment extends Avis |
function AvisManteniment() { return <Avis …/> } |
| Cal conèixer els mètodes interns del pare per sobreescriure'ls | Només cal conèixer les props públiques, el seu contracte documentat |
Canviar Avis pot trencar la subclasse de forma invisible |
Canviar Avis trenca, com a molt, el contracte de props, i l'error és evident |
| La subclasse no pot especialitzar dos pares | AvisManteniment pot usar Avis i Modal alhora |
| Difícil de provar per separat | Cada component es prova amb props (Mòdul 9) |
I el benefici pràctic: quan el dissenyador canviï l'aspecte dels avisos, toques Avis.jsx i els tres s'actualitzen. Quan el text de manteniment canviï, toques AvisManteniment.jsx i res més se n'assabenta.
children com a funció: les render props
children com a funció: les render propsHi ha un cas que children normal no cobreix: quan el contenidor té dades que el contingut necessita. Un contenidor que filtra una llista sap quins elements queden, però no com s'han de pintar.
La solució clàssica: passar una funció com a children, a la qual el contenidor crida amb les dades. Se'n diu render prop (o children as a function).
// src/components/LlistaFiltrable.jsx
import { useState } from 'react';
import estils from './LlistaFiltrable.module.css';
/**
* Contenidor que filtra una col·lecció per text i delega el pintat.
* Props:
* - elements (array, obligatori)
* - campCerca (funció, obligatòria): d'un element retorna el text cercable
* - etiqueta (cadena, opcional): text del camp de cerca
* - children (FUNCIÓ, obligatòria): rep els elements filtrats i retorna JSX
*/
function LlistaFiltrable({ elements, campCerca, etiqueta = 'Cerca', children }) {
const [text, setText] = useState(''); // estat LOCAL: no es puja (04-01)
const terme = text.trim().toLowerCase();
const filtrats =
terme === ''
? elements
: elements.filter((element) =>
campCerca(element).toLowerCase().includes(terme)
);
return (
<div className={estils.contenidor}>
<label className={estils.etiqueta}>
{etiqueta}
<input
type="search"
value={text}
onChange={(esdeveniment) => setText(esdeveniment.target.value)}
/>
</label>
<p aria-live="polite" className={estils.recompte}>
{filtrats.length === 1
? '1 resultat'
: `${filtrats.length} resultats`}
</p>
{children(filtrats)} {/* ← aquí hi ha la clau: children és una funció */}
</div>
);
}
export default LlistaFiltrable;Ús amb bicicletes:
<LlistaFiltrable
elements={bicicletes}
campCerca={(bicicleta) => bicicleta.model}
etiqueta="Cerca per model"
>
{(visibles) => (
<LlistaBicicletes bicicletes={visibles} estacions={estacions} />
)}
</LlistaFiltrable>I el mateix contenidor, sense tocar-lo, amb estacions:
<LlistaFiltrable
elements={estacions}
campCerca={(estacio) => `${estacio.nom} ${estacio.barri}`}
etiqueta="Cerca per estació o barri"
>
{(visibles) => (
<ul>
{visibles.map((estacio) => (
<li key={estacio.id}>
<TargetaEstacio estacio={estacio} />
</li>
))}
</ul>
)}
</LlistaFiltrable>Què passa, línia a línia:
childrendeixa de ser JSX i passa a ser una funció. JSX admet qualsevol valor entre claus, inclosa una funció; l'única cosa que canvia és que el contenidor la crida en lloc de pintar-la.children(filtrats)és una crida normal de JavaScript. El contenidor decideix quines dades entrega; qui l'usa decideix com es pinten.- La lògica de filtratge viu en un sol lloc i serveix per a bicicletes, estacions o reserves.
- L'estat
textno puja, en aplicació directa de 04-01: ningú fora del contenidor el necessita.
Nota important. Avui aquest problema gairebé sempre es resol millor amb un hook personalitzat:
const { text, setText, filtrats } = useFiltreText(elements, campCerca). La lògica es reutilitza igual, però sense afegir un nivell a l'arbre de components i sense anidar funcions dins del JSX. Ho veuràs a Hooks Personalitzats. Les render props continuen sent útils quan, a més de la dada, hi ha marcatge per compartir —el camp de cerca i el recompte, en aquest exemple—, i apareixen en biblioteques molt usades, així que cal saber llegir-les.
- Components d'ordre superior (HOC): com es llegeixen
Un component d'ordre superior (higher-order component) és una funció que rep un component i en retorna un altre amb capacitats afegides. No és una funció de React: és un patró, igual que els decoradors d'altres llenguatges.
Es reconeixen a l'instant pel seu nom (amb…, o with… en anglès) i per la doble crida en exportar.
// src/hoc/ambAutenticacio.jsx — patró heretat, per saber-lo llegir
/**
* Envolta un component i li exigeix un usuari autenticat.
* ambAutenticacio(Component) -> Component protegit
*/
function ambAutenticacio(Component) {
function ComponentProtegit(props) {
const usuari = llegirUsuariDeSessio(); // font fictícia
if (!usuari) {
return <Avis to="advertencia" titol="Accés restringit">
Inicia sessió per veure aquesta secció de CicloUrbano.
</Avis>;
}
return <Component {...props} usuari={usuari} />;
}
// Bona pràctica: nom llegible a React DevTools
ComponentProtegit.displayName = `ambAutenticacio(${Component.name})`;
return ComponentProtegit;
}
export default ambAutenticacio;// Ús
import ambAutenticacio from '../hoc/ambAutenticacio.jsx';
function PanellOperari({ usuari, bicicletes }) {
return <p>Hola, {usuari.nom}. Hi ha {bicicletes.length} bicicletes a la flota.</p>;
}
export default ambAutenticacio(PanellOperari);Claus per llegir codi amb HOC:
- El component exportat no és el que has escrit, sinó l'embolcall. Per això a les DevTools veus
ambAutenticacio(PanellOperari)i noPanellOperaria seques. {...props}és l'spread de 02-03, i és obligatori: sense ell, l'embolcall es menja les props i el component interior es queda sense dades.- El HOC injecta props noves (
usuari), que apareixen «del no-res» a la signatura del component interior. Aquesta és justament la crítica principal: llegintPanellOperarino hi ha manera de saber d'on surtusuari.
El problema s'agreuja en acumular-los:
Aquest apilament produeix l'infern d'embolcalls (wrapper hell) que es va esmentar a 02-02: un arbre amb cinc capes que no pinten res, props que apareixen sense origen visible, col·lisions de noms entre HOC i depuració incòmoda. Els hooks el substitueixen per una cosa molt més plana:
// El mateix, amb hooks (Mòdul 5)
function PanellOperari({ bicicletes }) {
const usuari = useUsuari(); // es veu d'on surt
const tema = useTema();
// …
}Sense embolcalls, sense props injectades, amb l'origen de cada dada a la vista a la pròpia línia. Per això els HOC han quedat desplaçats: no els escriguis en codi nou, però espera't a trobar-los en projectes amb anys al darrere (connect() de Redux, que veuràs a 07-05, és el HOC més famós de la història de React).
- Taula comparativa de les cinc tècniques
| Tècnica | Reutilitza | Llegibilitat | Quan usar-la | Estat a React 19 |
|---|---|---|---|---|
| Herència de components | Res útil | Dolenta: acobla per dins | Mai | Descartada; impossible amb funcions |
Composició amb children |
Estructura i estil | Excel·lent: es llegeix com HTML | Contenidors amb una zona de contingut | Recomanada, és l'opció per defecte |
| Espais amb nom | Estructura amb diverses zones | Molt bona: cada espai s'autoexplica | Capcalera, accions, peu, barra lateral | Recomanada |
| Render props | Lògica i marcatge alhora | Mitjana: anida funcions al JSX | Quan el contenidor aporta dades i marcatge | Vàlida, però minoritària |
| Components d'ordre superior | Lògica i comportament | Baixa: props d'origen invisible | Només per mantenir codi existent | Desplaçada pels hooks |
| Hooks personalitzats | Lògica amb estat, sense marcatge | Excel·lent: és una crida a funció | Lògica reutilitzable sense interfície pròpia | Recomanada (05-06) |
- La regla pràctica: composar interfície, extreure lògica
Tot l'anterior es resumeix en una frase que convé memoritzar:
Composa per a la interfície; extreu funcions o hooks per a la lògica.
Aplicada com a arbre de decisió:
flowchart TD
Q1{"Què vull reutilitzar?"}
Q1 -- "Marcatge i estructura" --> Q2{"Quantes zones de contingut?"}
Q2 -- "Una" --> C1["children"]
Q2 -- "Diverses" --> C2["Espais amb nom"]
Q1 -- "Una variant concreta<br/>d'un component" --> C3["Especialització<br/>per configuració"]
Q1 -- "Lògica SENSE estat" --> C4["Una funció normal<br/>a src/utilitats/"]
Q1 -- "Lògica AMB estat" --> C5["Hook personalitzat<br/>(05-06)"]
Q1 -- "Dades per a tot<br/>un subarbre" --> C6["Context<br/>(05-04)"]
Dos exemples ja presents a CicloUrbano confirmen els extrems del diagrama: classes() a src/utilitats/classes.js i validarReserva() a src/utilitats/validarReserva.js són lògica sense estat, i per això són funcions normals, no components ni HOC. No calia cap patró per reutilitzar-les: n'hi va haver prou d'exportar-les.
- Exemple central: la pantalla de catàleg recomposta
Ajuntem les peces. App va quedar així al final de 04-01, amb tota l'estructura escrita a mà:
// src/App.jsx — ABANS
return (
<>
<Capcalera />
<main>
<ResumFlota flota={bicicletes} />
<SelectorTipus tipusTriat={tipusTriat} alCanviarTipus={gestionarCanviTipus} />
<LlistaBicicletes bicicletes={bicicletesVisibles} estacions={estacions} … />
<PanellReserva bicicleta={bicicletaSeleccionada} … />
</main>
<PeuDePagina />
</>
);I així queda componint Disseny + PanellAvancat + AvisManteniment:
// src/App.jsx — DESPRÉS (fragment del return)
return (
<Disseny>
<ResumFlota flota={bicicletes} />
<PanellAvancat
titol="Catàleg de bicicletes"
capcalera={
<p aria-live="polite">
Mostrant {bicicletesVisibles.length} de {bicicletes.length} bicicletes.
</p>
}
accions={<SelectorTipus tipusTriat={tipusTriat} alCanviarTipus={gestionarCanviTipus} />}
peu={<small>Preus amb IVA inclòs · Tarifa mínima: 1 hora</small>}
>
<LlistaBicicletes
bicicletes={bicicletesVisibles}
estacions={estacions}
alSeleccionar={gestionarSeleccioBicicleta}
alReservar={gestionarReserva}
/>
</PanellAvancat>
<PanellAvancat titol="Reserva">
{bicicletaSeleccionada && bicicletaSeleccionada.estat === 'mantenimiento' ? (
<AvisManteniment
bicicleta={bicicletaSeleccionada}
alAvisarOperari={gestionarAvisOperari}
/>
) : (
<PanellReserva
bicicleta={bicicletaSeleccionada}
hores={horesReserva}
alCanviarHores={gestionarCanviHores}
alConfirmar={gestionarConfirmacio}
/>
)}
</PanellAvancat>
</Disseny>
);L'arbre resultant:
flowchart TD
APP["App<br/>estat: tipusTriat, bicicletaSeleccionada, horesReserva"]
APP --> DIS["Disseny<br/>(children)"]
DIS --> CAB["Capcalera"]
DIS --> MAIN["main"]
DIS --> PIE["PeuDePagina"]
MAIN --> RES["ResumFlota"]
MAIN --> PA1["PanellAvancat «Catàleg»"]
MAIN --> PA2["PanellAvancat «Reserva»"]
PA1 -- "accions" --> SEL["SelectorTipus"]
PA1 -- "children" --> LIS["LlistaBicicletes"]
PA2 -- "children" --> PR["PanellReserva o AvisManteniment"]
AV["AvisManteniment"] --> AVG["Avis (genèric)"]
Què s'ha guanyat, en termes concrets:
Apptorna a llegir-se com un índex de la pantalla, l'objectiu que es va fixar a 02-01. L'estructura visual (capçalera, main, peu) ha anat aDissenyi no es repetirà a cada pantalla nova que afegeixis.- Ni una sola herència. Cinc components reutilitzats, zero
extends. Avisés l'únic que coneix els colors, els símbols i els rols ARIA dels missatges;AvisMantenimentiAvisReservaCreadanomés hi aporten el seu text i el seu to.- Els espais amb nom acosten el filtre al que filtra:
SelectorTipuses pinta a la capçalera del panell del catàleg, encara que el seu estat continuï vivint aApp. Posició i propietat de la dada són coses independents.
Errors Comuns i Consells
- Intentar
extends MiComponent. No compila amb components de funció i no aporta res amb classes. Si sents la necessitat, gairebé sempre busques un contenidor ambchildreno una especialització per configuració. - Oblidar l'spread en un HOC.
<Component usuari={usuari} />sense{...props}deixa el component interior sense les props que li arribaven de fora, i el fallo es manifesta com a dades buides sense cap error. - Convertir
childrenen funció sense avisar. SiLlistaFiltrablefachildren(filtrats)i algú li passa JSX normal, l'error éschildren is not a function. Documenta-ho al bloc de props i, si el component admet totes dues formes, comprovatypeof children === 'function'. - Crear un contenidor per a cada variació mínima. Abans d'escriure
PanellPetit,PanellGraniPanellVermell, prova amb una propvarianten un únicPanell. L'especialització es justifica quan hi ha contingut o comportament propis, no només una classe CSS diferent. - Anidar massa contenidors.
<Disseny><Panell><Targeta><Panell>…amb sis nivells complica la lectura tant com l'infern d'embolcalls que criticàvem. Si l'arbre es torna il·legible, extreu una pantalla completa al seu propi component. - Consell: component genèric primer, especialitzacions després. Escriu
Avisresolent el cas concret que tens al davant i extreuAvisMantenimentquan aparegui el segon ús. Generalitzar amb un sol cas porta a abstraccions equivocades. - Consell: les props que reben JSX es nomenen per la seva zona, no pel seu contingut.
accions,capcaleraipeudescriuen on va el contingut;botoReservardescriuria què és, i lligaria el contenidor a un cas concret.
Exercicis
Exercici 1. Crea TargetaResum, un contenidor amb espais amb nom i una única zona de contingut lliure. Props: titol (cadena), icona (JSX, opcional, es pinta a l'esquerra del títol amb aria-hidden), children (cos) i accions (JSX, opcional, al peu). Després usa'l dues vegades a la pantalla de CicloUrbano: una per al resum de la flota i una altra per a l'estació est-01.
Exercici 2. Partint de l'Avis genèric de l'apartat 4, crea AvisSenseResultats, que es mostri quan el filtre no retorna cap bicicleta. Ha de portar to amb valor info, el títol «Sense resultats», un text que esmenti el tipus filtrat amb la seva etiqueta llegible i un botó «Veure totes» que cridi una prop alRestablir. Integra'l a App sense tocar LlistaBicicletes.
Exercici 3. Converteix aquest HOC en composició explícita. Explica què millora i què es perd.
function ambPanell(Component, titol) {
return function ComponentAmbPanell(props) {
return (
<Panell titol={titol}>
<Component {...props} />
</Panell>
);
};
}
export default ambPanell(LlistaBicicletes, 'Catàleg');Solucions
Solució 1.
// src/components/TargetaResum.jsx
import estils from './TargetaResum.module.css';
/**
* Targeta de resum amb icona, cos lliure i accions.
* Props:
* - titol (cadena, obligatori)
* - icona (JSX, opcional)
* - children (contingut)
* - accions (JSX, opcional)
*/
function TargetaResum({ titol, icona, children, accions }) {
return (
<article className={estils.targeta}>
<header className={estils.capcalera}>
{icona && <span className={estils.icona} aria-hidden="true">{icona}</span>}
<h3 className={estils.titol}>{titol}</h3>
</header>
<div className={estils.cos}>{children}</div>
{accions && <footer className={estils.accions}>{accions}</footer>}
</article>
);
}
export default TargetaResum;// src/App.jsx (fragment)
<TargetaResum titol="Flota" icona="🚲">
<ResumFlota flota={bicicletes} />
</TargetaResum>
<TargetaResum
titol={estacions[0].nom}
icona="📍"
accions={<button type="button" onClick={() => gestionarVeureEstacio(estacions[0])}>Veure bicicletes</button>}
>
<p>Barri: {estacions[0].barri}</p>
<p>Places: {estacions[0].places}</p>
</TargetaResum>El mateix contenidor serveix per a dos continguts que no tenen res a veure, sense cap línia condicional a dins: això és composició funcionant.
Solució 2.
// src/components/AvisSenseResultats.jsx
import Avis from './Avis.jsx';
const ETIQUETES = {
todos: 'Totes',
urbana: 'Urbanes',
electrica: 'Elèctriques',
carga: 'De càrrega'
};
/**
* Props:
* - tipusTriat (cadena, obligatori)
* - alRestablir (funció, obligatòria)
*/
function AvisSenseResultats({ tipusTriat, alRestablir }) {
return (
<Avis
to="info"
titol="Sense resultats"
accions={<button type="button" onClick={alRestablir}>Veure totes</button>}
>
No hi ha bicicletes del tipus <strong>{ETIQUETES[tipusTriat]}</strong> al catàleg
ara mateix.
</Avis>
);
}
export default AvisSenseResultats;// src/App.jsx (fragment)
{bicicletesVisibles.length === 0 ? (
<AvisSenseResultats
tipusTriat={tipusTriat}
alRestablir={() => setTipusTriat('todos')}
/>
) : (
<LlistaBicicletes
bicicletes={bicicletesVisibles}
estacions={estacions}
alSeleccionar={gestionarSeleccioBicicleta}
alReservar={gestionarReserva}
/>
)}LlistaBicicletes no es toca: continua amb el seu propi estat buit genèric per quan l'usin altres pantalles, i App decideix aquí un missatge més específic perquè és qui coneix el filtre. Fixa't que ETIQUETES està duplicat en dos fitxers; si el projecte creix, el lloc natural per a aquesta constant seria src/dades/domini.js.
Solució 3.
// Composició explícita: s'usa directament on calgui
<Panell titol="Catàleg">
<LlistaBicicletes
bicicletes={bicicletesVisibles}
estacions={estacions}
alSeleccionar={gestionarSeleccioBicicleta}
alReservar={gestionarReserva}
/>
</Panell>Què millora: el títol deixa d'estar congelat en el moment de crear el component i pot dependre de l'estat —un titol calculat amb el nombre de resultats, per exemple—; desapareix un nivell de l'arbre i un nom estrany a les DevTools; i qui llegeix el JSX veu d'una ullada que hi ha un panell embolicant la llista, sense obrir cap altre fitxer.
Què es perd: si el mateix panell amb el mateix títol es repetís en quinze llocs, el HOC evitaria repetir dues línies. La resposta idiomàtica a això no és el HOC, sinó una especialització per configuració: function PanellCataleg({ children }) { return <Panell titol="Catàleg">{children}</Panell>; }, que es llegeix com JSX normal i conserva tota la flexibilitat.
Conclusió
React substitueix l'herència per la composició, i no li falta cap capacitat per això. children cobreix els contenidors d'una sola zona (Panell, Modal, Disseny); els espais amb nom cobreixen els de diverses (capcalera, accions, peu); l'especialització per configuració substitueix les subclasses (AvisManteniment a partir d'Avis); les render props resolen el cas en què el contenidor aporta dades a més de marcatge, encara que avui solen cedir el lloc a un hook personalitzat; i els components d'ordre superior es llegeixen però no s'escriuen, excepte per mantenir codi existent. La regla que ordena tot això és una de sola: composar per a la interfície, extreure funcions o hooks per a la lògica.
La pantalla de catàleg de CicloUrbano ha quedat recomposta amb Disseny + PanellAvancat + contingut, amb els avisos sortint tots d'un Avis genèric, i App torna a llegir-se com un índex.
Queda pendent una cosa que les dues últimes lliçons han fregat sense abordar-la: els components neixen, canvien i moren. Un Modal que s'obre i es tanca, un panell d'activitat que necessita un temporitzador mentre és en pantalla i l'ha de donar de baixa en desaparèixer. Aquesta vida té fases, i durant anys es va gestionar amb els mètodes del cicle de vida dels components de classe, que encara avui poblen les bases de codi reals. La pròxima lliçó és Mètodes del Cicle de Vida de React, on aprendràs a llegir-los, a entendre quin problema resolien i a traduir-los al model mental que fa servir React modern.
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
