El mòdul anterior es va tancar amb un deute molt concret: SelectorTipus guarda el tipus triat en el seu propi estat i avisa el pare, però el catàleg continua mostrant les cinc bicicletes fem el que fem; i TargetaBicicleta avisa amb alSeleccionar, però App només pot fer un console.log perquè no té on guardar la bicicleta seleccionada. Les dues coses fallen pel mateix motiu: l'estat és privat de cada component, i dos germans mai poden veure's l'estat l'un a l'altre. En aquesta lliçó aprendràs la tècnica que resol això —elevar l'estat (lifting state up)—, l'aplicaràs perquè el filtre de CicloUrbano funcioni de debò, entendràs per què duplicar la mateixa dada en dos llocs és una font garantida d'errors i veuràs el preu que es paga en elevar: la perforació de props.
Contingut
- El problema: dos germans i una dada compartida
- La regla: pujar a l'avantpassat comú més proper
- El refactor de CicloUrbano, pas a pas
Appamb estat:tipusTriatibicicletaSeleccionada- Estat derivat:
bicicletesVisiblesno és estat - El flux unidireccional, vist sencer
- Una única font de veritat: l'error de duplicar
- Components controlats i no controlats, a nivell de component
- Què NO convé elevar
- El cost d'elevar: la perforació de props
- El problema: dos germans i una dada compartida
Aquest és l'arbre de CicloUrbano tal com va quedar al final del mòdul 3:
flowchart TD
APP["App<br/>(sense estat)"] --> CAB["Capcalera"]
APP --> RES["ResumFlota"]
APP --> SEL["SelectorTipus<br/><b>estat: tipusTriat</b>"]
APP --> LIS["LlistaBicicletes<br/>(sense estat)"]
APP --> PIE["PeuDePagina"]
LIS --> T1["TargetaBicicleta"]
LIS --> T2["TargetaBicicleta"]
SEL -. "com arriba<br/>tipusTriat fins aquí?" .-x LIS
La fletxa creuada és el problema. tipusTriat viu dins de SelectorTipus, i des d'allà la dada només pot viatjar cap avall (als fills de SelectorTipus, que no en té) o cap amunt (avisant el pare amb una prop de funció). El que no existeix a React és un camí lateral: un component no pot llegir l'estat del seu germà.
I no és una limitació capritxosa. Si LlistaBicicletes pogués llegir l'estat de SelectorTipus, tots dos quedarien acoblats: no podries reutilitzar la llista en una altra pantalla sense arrossegar el selector, i per entendre per què la llista mostra el que mostra hauries de llegir un component que no apareix en el seu codi. React tria la restricció a propòsit, i a canvi ofereix una regla senzilla per resoldre-ho.
- La regla: pujar a l'avantpassat comú més proper
Quan dos o més components necessiten la mateixa dada, aquesta dada ha de viure en l'estat de l'avantpassat comú més proper, i baixar a cadascun com a prop.
La tècnica es diu elevar l'estat i consta de tres moviments:
| Pas | Què es fa | A CicloUrbano |
|---|---|---|
| 1. Identificar | Quins components necessiten la dada? | SelectorTipus (per pintar el botó actiu) i LlistaBicicletes (per filtrar) |
| 2. Localitzar | Quin és el seu avantpassat comú més proper? | App |
| 3. Moure | L'estat puja; la dada baixa com a prop; el fill avisa amb un callback | tipusTriat passa de SelectorTipus a App |
El component que perd l'estat no es queda mut: rep dues props en el seu lloc.
- El valor actual (
tipusTriat), per pintar-se. - Una funció d'avís (
alCanviarTipus), per demanar al pare que el canviï.
Si això et sona, és perquè és exactament el patró que vas fer servir a 03-04 amb un <input> controlat: value + onChange. La diferència és que allà el component controlat era una etiqueta del DOM i aquí és un component teu. El patró és el mateix, i hi tornarem a l'apartat 8.
- El refactor de CicloUrbano, pas a pas
Pas 1: SelectorTipus deixa de guardar estat
Aquesta és la versió de 03-01, la que substituirem:
// src/components/SelectorTipus.jsx — VERSIÓ A SUBSTITUIR
function SelectorTipus({ alCanviarTipus }) {
const [tipusTriat, setTipusTriat] = useState('todos'); // <- l'estat que fa nosa
function gestionarSeleccioTipus(tipus) {
setTipusTriat(tipus);
if (alCanviarTipus) {
alCanviarTipus(tipus);
}
}
// …
}I aquesta és la nova:
// src/components/SelectorTipus.jsx
import { classes } from '../utilitats/classes.js';
import estils from './SelectorTipus.module.css';
const TIPUS = ['todos', 'urbana', 'electrica', 'carga'];
const ETIQUETES = {
todos: 'Totes',
urbana: 'Urbanes',
electrica: 'Elèctriques',
carga: 'De càrrega'
};
/**
* Selector del tipus de bicicleta. Component CONTROLAT: no guarda estat.
* Props:
* - tipusTriat (cadena, obligatori): el tipus actiu ara mateix
* - alCanviarTipus (funció, obligatòria): rep el tipus premut
*/
function SelectorTipus({ tipusTriat, alCanviarTipus }) {
function gestionarSeleccioTipus(tipus) {
alCanviarTipus(tipus);
}
function gestionarTeclaNetejar(esdeveniment) {
if (esdeveniment.key === 'Escape') {
alCanviarTipus('todos');
}
}
return (
<div className={estils.selector} onKeyDown={gestionarTeclaNetejar}>
<p className={estils.titol}>Filtra per tipus:</p>
{TIPUS.map((tipus) => (
<button
key={tipus}
type="button"
className={classes(estils.boto, tipus === tipusTriat && estils.actiu)}
onClick={() => gestionarSeleccioTipus(tipus)}
aria-pressed={tipus === tipusTriat}
>
{ETIQUETES[tipus]}
</button>
))}
<p className={estils.seleccio}>
Selecció actual: <strong>{ETIQUETES[tipusTriat]}</strong>
</p>
</div>
);
}
export default SelectorTipus;Canvis i per què:
- Desapareixen
useStatei el seuimport. El component ja no recorda res: tot el que pinta surt de les seves props. S'ha convertit en un component de presentació pur, dels que vas veure a 02-01. alCanviarTipuspassa de opcional a obligatòria. Abans era un afegit: el selector funcionava sense ella perquè es pintava amb el seu propi estat. Ara és l'única via de canvi; sense aquesta prop el component seria un adorn que no respon. Documenta-ho al bloc/** Props: … */.- El botó actiu es decideix amb
tipusTriat, que ara arriba de fora. El JSX no ha canviat ni una línia: li és igual d'on vingui el valor. aria-pressedcomunica l'estat del botó a les tecnologies d'assistència, seguint el que vas aprendre a 03-06: la selecció no es pot informar només amb el color del CSS.
Pas 2: App recull l'estat
// src/App.jsx
import { useState } from 'react';
import { bicicletes, estacions } from './dades/domini.js';
import Capcalera from './components/Capcalera.jsx';
import ResumFlota from './components/ResumFlota.jsx';
import SelectorTipus from './components/SelectorTipus.jsx';
import LlistaBicicletes from './components/LlistaBicicletes.jsx';
import PanellReserva from './components/PanellReserva.jsx';
import PeuDePagina from './components/PeuDePagina.jsx';
function App() {
const [tipusTriat, setTipusTriat] = useState('todos');
const [bicicletaSeleccionada, setBicicletaSeleccionada] = useState(null);
// Valor DERIVAT: es recalcula a cada render, no es guarda
const bicicletesVisibles =
tipusTriat === 'todos'
? bicicletes
: bicicletes.filter((bicicleta) => bicicleta.tipus === tipusTriat);
function gestionarCanviTipus(tipus) {
setTipusTriat(tipus);
}
function gestionarSeleccioBicicleta(bicicleta) {
setBicicletaSeleccionada(bicicleta);
}
function gestionarReserva(bicicleta) {
setBicicletaSeleccionada(bicicleta);
}
function gestionarConfirmacio({ bicicleta, hores, total }) {
console.log('Reserva confirmada', bicicleta.id, hores, total);
setBicicletaSeleccionada(null);
}
return (
<>
<Capcalera />
<main>
<ResumFlota flota={bicicletes} />
<SelectorTipus tipusTriat={tipusTriat} alCanviarTipus={gestionarCanviTipus} />
<LlistaBicicletes
bicicletes={bicicletesVisibles}
estacions={estacions}
alSeleccionar={gestionarSeleccioBicicleta}
alReservar={gestionarReserva}
/>
<PanellReserva
bicicleta={bicicletaSeleccionada}
hores={2}
alConfirmar={gestionarConfirmacio}
/>
</main>
<PeuDePagina />
</>
);
}
export default App;El que ha passat aquí és tot el mòdul condensat en un fitxer:
Appper fi té estat, i són exactament dues dades: quin filtre està actiu i quina bicicleta està seleccionada. Ni una més.LlistaBicicletesrepbicicletesVisibles, nobicicletes. El component no s'ha modificat: continua rebent un array i pintant-lo ambmap. Ni tan sols sap que existeix un filtre, i aquesta ignorància és una virtut, perquè el fa reutilitzable en qualsevol pantalla.PanellReservarep per fi una bicicleta de debò. Els retorns anticipats que vas escriure a 03-02 —«Selecciona una bicicleta del catàleg» quan falta la prop— deixen de ser una hipòtesi: ambbicicletaSeleccionadaanullen arrencar, aquest és literalment el primer estat que veu la persona usuària.gestionarConfirmacioneteja la selecció posant-la anull, amb la qual cosa el panell torna al seu missatge inicial. Un cicle complet, sense trucs.
I LlistaBicicletes es beneficia d'una cosa que ja tenia escrita: el seu estat buit. Si filtres per «De càrrega» i l'única bicicleta de càrrega és al taller, el bicicletes.length === 0 que vas programar a 03-03 es dispara i mostra el missatge. No has hagut d'escriure res de nou.
L'arbre, després
flowchart TD
APP["App<br/><b>estat: tipusTriat</b><br/><b>estat: bicicletaSeleccionada</b>"]
APP -- "tipusTriat ▼" --> SEL["SelectorTipus<br/>(sense estat)"]
APP -- "bicicletesVisibles ▼" --> LIS["LlistaBicicletes"]
APP -- "bicicleta ▼" --> PAN["PanellReserva"]
SEL -. "alCanviarTipus(tipus) ▲" .-> APP
LIS --> TAR["TargetaBicicleta"]
TAR -. "alSeleccionar(bicicleta) ▲" .-> APP
Les dades baixen per props (fletxes contínues) i els avisos pugen per funcions (fletxes discontínues). No hi ha cap fletxa horitzontal: els germans continuen sense parlar-se, es comuniquen a través del pare.
App amb estat: què guardar i què no
App amb estat: què guardar i què noQuan l'estat puja, la temptació és pujar-ho tot. Aplica aquest filtre a cada dada abans de convertir-la en estat del pare:
| Pregunta | Si la resposta és sí… |
|---|---|
| Canvia amb el temps per acció de la persona usuària? | Pot ser estat |
| El necessita més d'un component? | Ha de viure a l'avantpassat comú |
| Es pot calcular a partir d'un altre estat o de les props? | No és estat: és un valor derivat |
| Només l'usa un component i ningú més? | Deixa'l dins d'aquest component |
A CicloUrbano, tipusTriat i bicicletaSeleccionada passen les dues primeres preguntes i no passen la tercera: no hi ha manera de deduir-los de res. Són estat legítim.
Un detall sobre bicicletaSeleccionada: hi guardem l'objecte sencer, no el seu id. Amb dades estàtiques importades de domini.js funciona perfectament. Quan les dades vinguin d'un servidor i puguin actualitzar-se (Mòdul 7), la pràctica recomanada serà guardar l'id i buscar l'objecte al vol, perquè un objecte guardat en estat és una còpia congelada que no s'assabenta si l'original canvia. És el mateix avís sobre duplicació que apareix a l'apartat 7.
- Estat derivat:
bicicletesVisibles no és estat
bicicletesVisibles no és estatAquest és l'error més comú en elevar. La versió incorrecta:
// INCORRECTE: dos estats que cal mantenir sincronitzats a mà
const [tipusTriat, setTipusTriat] = useState('todos');
const [bicicletesVisibles, setBicicletesVisibles] = useState(bicicletes);
function gestionarCanviTipus(tipus) {
setTipusTriat(tipus);
setBicicletesVisibles(
tipus === 'todos' ? bicicletes : bicicletes.filter((b) => b.tipus === tipus)
);
}Funciona… fins que algú afegeix una altra manera de canviar el filtre i s'oblida de la segona línia. En aquell moment el selector diu «Elèctriques» i la llista mostra totes les bicicletes. El fallo no és al filtratge: és que la mateixa informació es guarda dues vegades i res no garanteix que coincideixin.
La versió correcta ja l'has vist: una sola línia, sense estat.
const bicicletesVisibles =
tipusTriat === 'todos'
? bicicletes
: bicicletes.filter((bicicleta) => bicicleta.tipus === tipusTriat);Es recalcula a cada render, que és exactament quan cal, i és impossible que es desincronitzi perquè no hi ha res a sincronitzar. És la mateixa lliçó de 02-04 —estat davant de valor derivat— aplicada ara a un component contenidor.
«No és ineficient filtrar a cada render?» Amb cinc bicicletes, o amb cinc-centes, no.
filtersobre un array petit costa microsegons, molt menys que la complexitat de mantenir dos estats a mà. Si algun dia el càlcul és realment costós i ho has mesurat, existeixuseMemo(08-03). Mesura primer, optimitza després.
- El flux unidireccional, vist sencer
Seguim un clic des del principi fins al final. Algú prem «Elèctriques»:
sequenceDiagram
participant U as Persona usuària
participant S as SelectorTipus
participant A as App
participant L as LlistaBicicletes
U->>S: clic a «Elèctriques»
S->>A: alCanviarTipus('electrica')
A->>A: setTipusTriat('electrica')
Note over A: React programa un nou render
A->>A: bicicletesVisibles = filter(...)
A->>S: tipusTriat = 'electrica' (botó actiu)
A->>L: bicicletes = [bici-002, bici-005]
L->>U: dues targetes en pantalla
Fixa't en un detall important: SelectorTipus no canvia res per si mateix. Prems el botó i no passa res visible fins que App actualitza el seu estat i torna a renderitzar. El selector només demana el canvi. Si App decidís ignorar la petició —per exemple, prohibint el filtre «De càrrega» als clients—, el botó no s'activaria. Tota l'autoritat és en un únic lloc, i això fa que el comportament sigui predictible i depurable: si alguna cosa es veu malament, l'estat equivocat és a App.
Aquest cicle tancat és el que es coneix com a flux de dades unidireccional, i és el motiu pel qual una aplicació React gran es pot raonar llegint de dalt a baix.
- Una única font de veritat: l'error de duplicar
El principi s'enuncia així: cada dada ha de tenir exactament un propietari. Qualsevol altre component que la necessiti la rep com a prop; ningú en fa una còpia local.
Vegem el fallo amb un exemple reproduïble. Imagina't que, després d'elevar l'estat, deixes també una còpia dins del fill «per comoditat»:
// INCORRECTE: el fill copia la prop en el seu propi estat
function SelectorTipus({ tipusTriat, alCanviarTipus }) {
const [tipusLocal, setTipusLocal] = useState(tipusTriat); // <- la còpia verinosa
function gestionarSeleccioTipus(tipus) {
setTipusLocal(tipus);
alCanviarTipus(tipus);
}
// … pinta el botó actiu fent servir tipusLocal
}A primer cop d'ull funciona. Però conté dues bombes de rellotgeria:
useState(tipusTriat)només llegeix la prop en el primer render. És el valor inicial, no un vincle permanent. Si demàAppafegeix un botó «Restablir filtres» que fasetTipusTriat('todos'), el pare dirà «todos» i el selector continuarà mostrant «Elèctriques» actiu. Per sempre.- Hi ha dues veritats alhora. Quin és el tipus triat de debò? Depèn de a qui preguntis. I depurar això significa mirar dos components a les DevTools i comparar.
La regla pràctica, sense excepcions que valgui la pena aprendre's ara:
| Situació | Què fer |
|---|---|
| El fill necessita mostrar una dada del pare | Rebre-la com a prop i fer-la servir directament |
| El fill necessita canviar aquesta dada | Cridar una funció que li passa el pare |
| El fill necessita una dada que ningú més fa servir | useState dins del fill, sense culpa |
| El fill necessita «inicialitzar-se» amb una prop i després anar per lliure | Replanteja el disseny; gairebé sempre la dada havia d'estar a dalt |
- Components controlats i no controlats, a nivell de component
A 03-04 i 03-05 vas aplicar aquests termes als camps d'un formulari. Ara s'apliquen igual a components que escrius tu, i la distinció és exactament la mateixa.
| Component controlat | Component no controlat | |
|---|---|---|
| On viu la dada | Al pare | Dins del propi component |
| Quines props rep | El valor + un callback de canvi | Com a molt, un valor inicial |
| Qui mana | El pare | El component |
| Avantatge | El pare pot llegir-lo, coordinar-lo, restablir-lo | S'usa amb una sola línia, sense cerimònia |
| Inconvenient | El pare ha de declarar estat | Ningú més pot veure ni canviar la dada |
| Exemple a CicloUrbano | SelectorTipus (després d'aquesta lliçó) |
Acordio amb obertPerDefecte |
Un mateix component pot admetre les dues modalitats, i és un patró que veuràs en moltes biblioteques. La tècnica: si la prop de valor arriba definida, el component obeeix el pare; si no, gestiona el seu propi estat.
// src/components/Acordio.jsx
/**
* Secció plegable. Admet dos modes:
* - Controlat: <Acordio obert={obert} alAlternar={...} />
* - No controlat: <Acordio obertPerDefecte />
* Props:
* - titol (cadena, obligatori)
* - children (contingut)
* - obert (booleà, opcional): activa el mode controlat
* - obertPerDefecte (booleà, opcional): valor inicial del mode no controlat
* - alAlternar (funció, opcional): rep el nou valor
*/
function Acordio({ titol, children, obert, obertPerDefecte = false, alAlternar }) {
const [obertIntern, setObertIntern] = useState(obertPerDefecte);
const esControlat = obert !== undefined;
const estaObert = esControlat ? obert : obertIntern;
function gestionarAlternar() {
if (!esControlat) {
setObertIntern(!estaObert);
}
if (alAlternar) {
alAlternar(!estaObert);
}
}
return (
<section>
<h3>
<button type="button" onClick={gestionarAlternar} aria-expanded={estaObert}>
{titol}
</button>
</h3>
{estaObert && <div>{children}</div>}
</section>
);
}
export default Acordio;Les tres línies clau:
const esControlat = obert !== undefined;El contracte és explícit: passar la prop significa «jo mano». Es compara ambundefinedi no amb un valor falsy, perquèobert={false}és un valor perfectament vàlid que sí que ha d'activar el mode controlat.const estaObert = esControlat ? obert : obertIntern;La resta del component fa servir sempre aquesta variable i no s'assabenta de quin mode està actiu. Tota la bifurcació cap en una línia.if (!esControlat) setObertIntern(...)En mode controlat el component no toca el seu estat intern: només avisa. Actualitzar-los tots dos seria duplicar la veritat, l'error de l'apartat 7.
No cal que tots els teus components admetin les dues modalitats —afegeix complexitat— però convé saber-lo llegir, perquè és el disseny de gairebé qualsevol component de biblioteca que facis servir.
- Què NO convé elevar
Elevar és una eina, no un dogma. Pujar estat que ningú més necessita empitjora el codi: obliga App a tornar a renderitzar l'arbre sencer per canvis que només afecten un racó, i omple el component arrel de dades irrellevants.
Casos clars d'estat que ha de quedar-se avall:
| Estat local | Per què es queda | On viu |
|---|---|---|
| Text escrit en un cercador amb retard (debounce) | El pare només necessita el terme ja estabilitzat, no cada pulsació | Al cercador; s'avisa a dalt en estabilitzar-se |
| Si un panell està plegat o desplegat | Ningú més decideix res amb aquesta dada | Al panell |
| Si un menú desplegable està obert | Purament visual i efímer | Al menú |
| L'índex de la pestanya activa dins d'un widget aïllat | Detall de presentació | Al widget |
| Si el ratolí és al damunt d'una targeta | Efímer i per instància | A la targeta |
El cercador amb retard ho il·lustra bé:
// src/components/CercadorBicicletes.jsx
/**
* Props:
* - alCercar (funció): rep el terme, però només quan es deixa d'escriure
*/
function CercadorBicicletes({ alCercar }) {
const [text, setText] = useState(''); // ESTAT LOCAL: no puja
function gestionarCanvi(esdeveniment) {
setText(esdeveniment.target.value);
// l'avís al pare es farà amb retard; el mecanisme el veuràs a 05-02
}
return (
<input
type="search"
value={text}
onChange={gestionarCanvi}
aria-label="Cerca bicicletes per model"
/>
);
}Si text visqués a App, cada tecla premuda provocaria un render de l'arbre complet, inclòs el catàleg sencer. En quedar-se dins, només es torna a pintar l'<input>, i el pare se n'assabenta una vegada, quan hi ha alguna cosa a fer.
La pregunta de control és sempre la mateixa: algú més necessita aquesta dada per pintar-se o per decidir alguna cosa? Si la resposta és no, es queda on és. I si demà la resposta canvia, elevar-la és un refactor de deu minuts: no cal anticipar-se.
- El cost d'elevar: la perforació de props
Elevar té un preu, i és honest conèixer-lo. Quan l'avantpassat comú és lluny, la dada ha de travessar tots els components intermedis, encara que a ells no els serveixi de res. Se'n diu perforació de props (prop drilling).
Imagina't que CicloUrbano creix i la targeta necessita saber el rol de l'usuari (usr-02 és operari i pot marcar manteniment):
// App -> Cataleg -> LlistaBicicletes -> TargetaBicicleta
function App() {
const [usuari, setUsuari] = useState(usuaris[0]);
return <Cataleg usuari={usuari} />; // nivell 1
}
function Cataleg({ usuari }) { // no l'usa, només el passa
return <LlistaBicicletes usuari={usuari} bicicletes={bicicletes} />;
}
function LlistaBicicletes({ usuari, bicicletes }) { // tampoc l'usa
return bicicletes.map((bicicleta) => (
<TargetaBicicleta key={bicicleta.id} bicicleta={bicicleta} usuari={usuari} />
));
}
function TargetaBicicleta({ bicicleta, usuari }) { // aquí sí que s'usa, per fi
return usuari.rol === 'operario' ? <BotoManteniment /> : null;
}flowchart TD
A["App<br/>estat: usuari"] -- "usuari" --> B["Cataleg<br/>❌ no l'usa"]
B -- "usuari" --> C["LlistaBicicletes<br/>❌ no l'usa"]
C -- "usuari" --> D["TargetaBicicleta<br/>✅ l'usa"]
Els símptomes són reconeixibles:
- Components intermedis amb props que només reenvien, sense fer-les servir.
- Afegir una dada nova obliga a tocar quatre fitxers perquè arribi a un.
- Reanomenar la prop obliga a un cercar-i-reemplaçar per tota la cadena.
- Els components intermedis perden reutilització: exigeixen una prop que no els importa.
Amb dos nivells no és un problema, i no convé arreglar-ho abans que faci mal. Amb quatre o cinc, sí. React té una resposta per a això: l'API de context, que permet posar un valor a disposició de tot un subarbre sense anar de mà en mà. La veuràs a useContext i, a escala d'aplicació sencera, al Mòdul 7, on també apareixen els gestors d'estat externs. Aquí només hem posat nom al problema.
Errors Comuns i Consells
- Copiar una prop en l'estat del fill.
useState(props.valor)només llegeix la prop una vegada, en el primer render. A partir d'aquí les dues còpies se separen i mai més tornen a coincidir. Si necessites llegir i canviar la dada, rep-la per prop i avisa cap amunt. - Guardar en estat el que es pot calcular.
bicicletesVisibles, totals, comptadors, textos formatats: tot això és derivat. Cada estat afegit és una oportunitat més de desincronització. - Elevar massa amunt «per si de cas». El destí correcte és l'avantpassat comú més proper, no l'arrel. Posar-ho tot a
Appconverteix el component arrel en un abocador i provoca renders innecessaris. - Oblidar-se de passar el callback. Si converteixes un component en controlat i oblides la prop de canvi, obtindràs un component inert que no respon als clics, sense cap error a la consola. Documenta les props obligatòries i considera un avís en desenvolupament.
- Passar el valor però no el gestor (o al revés). Un component controlat necessita les dues props: amb una de sola queda a mitges, igual que un
<input value>senseonChangequeda de només lectura. - Consell: mira les DevTools. Selecciona
Appa React DevTools i veuràs els seus dos estats canviant en directe mentre premeu filtres i targetes. És la millor manera de confirmar que la font de la veritat és on creus. - Consell: anomena les props del contracte. El parell
tipusTriat/alCanviarTipuses llegeix comvalue/onChange. Mantenir aquesta simetria en tot el projecte fa que els components es facin servir sense consultar-ne el codi.
Exercicis
Exercici 1. Eleva l'estat del PanellReserva. Ara mateix rep hores com a prop fixa amb valor 2, així que no es pot canviar. Fes que App guardi horesReserva en el seu estat (valor inicial 2) i que PanellReserva rebi hores i una nova prop alCanviarHores, amb dos botons («−» i «+») que no baixin mai d'1 hora. El total s'ha de recalcular sol. Explica per què el total no ha de ser estat.
Exercici 2. Afegeix a App un comptador de resultats accessible. A sota del SelectorTipus, mostra un text del tipus «Es mostren 2 de 5 bicicletes», amb la concordança correcta en singular i plural, dins d'un element amb aria-live="polite" (03-06). No afegeixis cap estat nou: el text ha de sortir íntegrament de bicicletesVisibles i bicicletes.
Exercici 3. Detecta i corregeix la desincronització. Aquest component té l'error de l'apartat 7. Descriu-lo amb un cas concret que el faci fallar i reescriu-lo correctament.
function ResumSeleccio({ bicicletaSeleccionada }) {
const [model, setModel] = useState(
bicicletaSeleccionada ? bicicletaSeleccionada.model : 'Cap'
);
return <p>Bicicleta seleccionada: {model}</p>;
}Solucions
Solució 1.
// src/App.jsx (fragment)
const [horesReserva, setHoresReserva] = useState(2);
function gestionarCanviHores(novesHores) {
setHoresReserva(Math.max(1, novesHores));
}
<PanellReserva
bicicleta={bicicletaSeleccionada}
hores={horesReserva}
alCanviarHores={gestionarCanviHores}
alConfirmar={gestionarConfirmacio}
/>// src/components/PanellReserva.jsx (fragment del bloc final)
/**
* Props:
* - bicicleta (objecte, opcional)
* - hores (número, opcional, per defecte 1)
* - alCanviarHores (funció, opcional): rep el nou nombre d'hores
* - alConfirmar (funció, opcional): rep { bicicleta, hores, total }
*/
function PanellReserva({ bicicleta, hores = 1, alCanviarHores, alConfirmar }) {
// … clàusules de guarda de 03-02, sense canvis …
const total = bicicleta.preuHora * hores; // DERIVAT
const totalFormatat = total.toFixed(2).replace('.', ',');
return (
<div className={estils.panell}>
<h3>{bicicleta.model}</h3>
<div className={estils.hores}>
<button
type="button"
onClick={() => alCanviarHores(hores - 1)}
disabled={hores <= 1}
aria-label="Treure una hora"
>
−
</button>
<span>{hores === 1 ? '1 hora' : `${hores} hores`}</span>
<button type="button" onClick={() => alCanviarHores(hores + 1)} aria-label="Afegir una hora">
+
</button>
</div>
<p className={estils.total}>Total: {totalFormatat} €</p>
<button type="button" onClick={() => alConfirmar({ bicicleta, hores, total })}>
Confirmar reserva
</button>
</div>
);
}El total no és estat perquè es dedueix completament de bicicleta.preuHora i hores. Guardar-lo obligaria a recalcular-lo en dos llocs (en canviar les hores i en canviar de bicicleta) i n'hi hauria prou d'oblidar-ne un per mostrar un preu fals. El límit inferior s'aplica a App, la propietària de la dada, no al panell: així la regla viu al costat de l'estat.
Solució 2.
// src/App.jsx (fragment, just sota del SelectorTipus)
<p aria-live="polite" className="resultats">
{bicicletesVisibles.length === 1
? `Es mostra 1 bicicleta de ${bicicletes.length}.`
: `Es mostren ${bicicletesVisibles.length} bicicletes de ${bicicletes.length}.`}
</p>Sense estat nou: tots dos números surten d'arrays que ja existeixen. aria-live="polite" fa que el lector de pantalla anunciï el canvi en acabar de llegir el que estigui llegint, de manera que qui filtra amb el teclat sap quants resultats queden encara que no vegi la llista.
Solució 3. El fallo: useState pren el model una única vegada, en el primer render. Com que en arrencar bicicletaSeleccionada és null, model val 'Cap' per sempre; prem les cinc targetes i el text no canviarà mai. Ni tan sols un canvi posterior de bicicleta ho arregla, perquè l'estat ja està inicialitzat. És l'error clàssic de copiar una prop en l'estat.
// CORRECTE: sense estat; la dada ja la té el pare
function ResumSeleccio({ bicicletaSeleccionada }) {
const model = bicicletaSeleccionada ? bicicletaSeleccionada.model : 'Cap';
return <p>Bicicleta seleccionada: {model}</p>;
}El component passa a ser una funció pura de les seves props: per a les mateixes props, sempre la mateixa sortida. Impossible que es desincronitzi.
Conclusió
Elevar l'estat és la tècnica que converteix un conjunt de components solts en una aplicació. La regla cap en una frase: l'estat compartit viu a l'avantpassat comú més proper, baixa com a props i torna a pujar en forma d'avisos. Aplicant-la, CicloUrbano ha saldat el deute que arrossegava des del mòdul 2: SelectorTipus s'ha convertit en un component controlat sense estat propi, App guarda tipusTriat i bicicletaSeleccionada, bicicletesVisibles es calcula com a valor derivat —mai com a estat— i PanellReserva rep per fi una bicicleta real. El catàleg filtra de debò.
També has vist els límits de la tècnica. Elevar massa empitjora el codi: l'estat realment local —el text d'un cercador, si un panell està plegat— ha de quedar-se avall. I elevar lluny té un preu amb nom propi, la perforació de props, la resposta de la qual arribarà amb useContext i el Mòdul 7.
Queda una pregunta oberta que assoma tan bon punt la interfície creix: App ja comença a acumular seccions, i aviat necessitaràs embolicar el catàleg en un disseny amb capçalera i barra lateral, mostrar avisos de diversos tipus i crear panells reutilitzables. En altres llenguatges, la resposta seria l'herència: un PanellBase del qual heretin PanellCataleg i PanellReserves. A React aquesta resposta és l'equivocada, i la bona en té un altre nom. La pròxima lliçó és Composició vs Herència, on aprendràs a construir components a partir d'altres components sense heretar ni una sola vegada.
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
