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

  1. El problema: dos germans i una dada compartida
  2. La regla: pujar a l'avantpassat comú més proper
  3. El refactor de CicloUrbano, pas a pas
  4. App amb estat: tipusTriat i bicicletaSeleccionada
  5. Estat derivat: bicicletesVisibles no és estat
  6. El flux unidireccional, vist sencer
  7. Una única font de veritat: l'error de duplicar
  8. Components controlats i no controlats, a nivell de component
  9. Què NO convé elevar
  10. El cost d'elevar: la perforació de props

  1. 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.

  1. 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.

  1. 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 useState i el seu import. 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.
  • alCanviarTipus passa 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-pressed comunica 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:

  • App per fi té estat, i són exactament dues dades: quin filtre està actiu i quina bicicleta està seleccionada. Ni una més.
  • LlistaBicicletes rep bicicletesVisibles, no bicicletes. El component no s'ha modificat: continua rebent un array i pintant-lo amb map. Ni tan sols sap que existeix un filtre, i aquesta ignorància és una virtut, perquè el fa reutilitzable en qualsevol pantalla.
  • PanellReserva rep 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: amb bicicletaSeleccionada a null en arrencar, aquest és literalment el primer estat que veu la persona usuària.
  • gestionarConfirmacio neteja la selecció posant-la a null, 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.

  1. App amb estat: què guardar i què no

Quan 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.

  1. Estat derivat: bicicletesVisibles no és estat

Aquest é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. filter sobre 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, existeix useMemo (08-03). Mesura primer, optimitza després.

  1. 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.

  1. 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:

  1. useState(tipusTriat) només llegeix la prop en el primer render. És el valor inicial, no un vincle permanent. Si demà App afegeix un botó «Restablir filtres» que fa setTipusTriat('todos'), el pare dirà «todos» i el selector continuarà mostrant «Elèctriques» actiu. Per sempre.
  2. 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

  1. 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 amb undefined i 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.

  1. 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.

  1. 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 App converteix 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> sense onChange queda de només lectura.
  • Consell: mira les DevTools. Selecciona App a 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 / alCanviarTipus es llegeix com value / 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

Mòdul 2: Components de React

Mòdul 3: Treballar amb Esdeveniments

Mòdul 4: Conceptes Avançats de Components

Mòdul 5: Hooks de React

Mòdul 6: Enrutament a React

Mòdul 7: Gestió de l'Estat

Mòdul 8: Optimització del Rendiment

Mòdul 9: Proves a React

Mòdul 10: Temes Avançats

Mòdul 11: Projecte: Construir una Aplicació Completa

© Copyright 2026. Tots els drets reservats