Portem dos mòduls arrossegant la mateixa lletjor: LlistaBicicletes rep tres props anomenades primera, segona i tercera, i escriu tres targetes a mà. Amb cinc bicicletes al domini.js ja es queda curta; amb cinquanta seria insostenible. En aquesta lliçó es salda aquest deute i un altre de més antic: a la lliçó 01-05 va quedar dit que les llistes necessiten una identitat estable i que la mecànica completa arribaria aquí. Aprendràs a transformar un array de dades en un array d'elements JSX amb map, a entendre què és exactament una key i què li fa a l'algorisme de reconciliació, a reconèixer per què l'índex de l'array és gairebé sempre una mala clau —amb un exemple que pots reproduir i veure fallar—, i a combinar filter, sort i map sense destrossar les dades originals.

Contingut

  1. El problema: repetició escrita a mà
  2. map: d'un array de dades a un array d'elements
  3. Retornar JSX des de map: el return que s'oblida
  4. El refactor: LlistaBicicletes amb map
  5. Què és key i per què React la necessita
  6. Què serveix com a clau i què no
  7. L'índex de l'array: l'error reproduïble
  8. Claus en fragments
  9. filter i sort sense mutar l'array original
  10. Llistes imbricades: estacions amb les seves bicicletes
  11. L'estat buit d'una llista

  1. El problema: repetició escrita a mà

Aquest és el punt de partida, tal com va quedar al final de la lliçó anterior:

// src/components/LlistaBicicletes.jsx  — VERSIÓ A SUBSTITUIR
function LlistaBicicletes({ primera, segona, tercera, alSeleccionar, alReservar }) {
  return (
    <section className={estils.llista}>
      <h2>Bicicletes del catàleg</h2>
      {primera && <TargetaBicicleta bicicleta={primera} nomEstacio="Plaça Major" … />}
      {segona && <TargetaBicicleta bicicleta={segona} nomEstacio="Plaça Major" … />}
      {tercera && <TargetaBicicleta bicicleta={tercera} nomEstacio="Parc Nord" … />}
    </section>
  );
}

Els defectes són estructurals, no d'estil:

Defecte Conseqüència
El nombre d'elements està fixat al codi Les bicicletes 4 i 5 del domini.js no es veuen. Afegir-ne una exigeix tocar dos fitxers
Els noms de les props no signifiquen res primera no descriu una dada: descriu una posició
Repetició literal Canviar una prop de la targeta obliga a repetir el canvi tres vegades, amb el risc d'oblidar-ne una
Impossible filtrar o ordenar Qualsevol filtre hauria de reassignar manualment quina bicicleta va a cada forat
L'estat buit és artificial Cal comptar props en lloc de comptar dades

I el problema de fons: el nombre de targetes és una dada, no una decisió del programador. Ha de sortir de l'array.

  1. map: d'un array de dades a un array d'elements

Array.prototype.map és JavaScript estàndard, no una funció de React: recorre un array i retorna un altre array nou amb el resultat d'aplicar una funció a cada element. L'array original no es toca.

const preus = [2.5, 4.0, 5.5];
const ambIva = preus.map((preu) => preu * 1.21);
// preus continua sent [2.5, 4, 5.5]
// ambIva és [3.025, 4.84, 6.655]

La idea de React és exactament la mateixa, canviant números per elements JSX:

const models = ['Urbana Clàssica', 'Elèctrica Pro', 'Càrrega Max'];

const elements = models.map((model) => <li>{model}</li>);
// elements és un array de tres objectes element de React

I aquí entra en joc una propietat del JSX que potser no esperaves: React sap pintar un array d'elements. Si poses un array dins d'unes claus, React el recorre i pinta cada element en ordre, sense separadors ni comes.

function LlistaModels() {
  const models = ['Urbana Clàssica', 'Elèctrica Pro', 'Càrrega Max'];

  return (
    <ul>
      {models.map((model) => (
        <li key={model}>{model}</li>
      ))}
    </ul>
  );
}

El flux mental, en tres passos:

flowchart LR
    A["Array de DADES<br/>['Urbana Clàssica', …]"] -- ".map(…)" --> B["Array d'ELEMENTS<br/>[&lt;li&gt;, &lt;li&gt;, &lt;li&gt;]"]
    B -- "{ } dins del JSX" --> C["React pinta cadascun<br/>en ordre"]

Fixa't en el key={model}: és obligatori i ho explicarem a l'apartat 5. De moment acostuma't a escriure'l sempre que facis servir map per generar elements.

  1. Retornar JSX des de map: el return que s'oblida

La funció que passes a map ha de retornar alguna cosa. Amb funcions fletxa hi ha dues formes d'escriure-la, i confondre-les produeix una fallada desconcertant: la llista es queda buida sense cap error.

{/* 1. Cos d'expressió amb parèntesis: el return és IMPLÍCIT */}
{bicicletes.map((bicicleta) => (
  <TargetaBicicleta key={bicicleta.id} bicicleta={bicicleta} />
))}

{/* 2. Cos de bloc amb claus: el return és OBLIGATORI */}
{bicicletes.map((bicicleta) => {
  const preu = bicicleta.preuHora.toFixed(2);
  return <TargetaBicicleta key={bicicleta.id} bicicleta={bicicleta} preu={preu} />;
})}

{/* 3. L'ERROR: claus de bloc SENSE return */}
{bicicletes.map((bicicleta) => {
  <TargetaBicicleta key={bicicleta.id} bicicleta={bicicleta} />;   // ✘ no retorna res
})}

En el tercer cas, la funció retorna undefined per a cada element, així que map produeix [undefined, undefined, undefined], i React no pinta els undefined. Resultat: una secció buida, sense errors ni avisos. És una fallada clàssica que pot costar vint minuts.

Sintaxi Quan fer-la servir Compte
(x) => ( … ) El cas normal: només retornes JSX Els parèntesis no són obligatoris, però eviten errors de format
(x) => { … return … } Quan necessites calcular variables abans Mai t'oblidis del return
(x) => { … } sense return Mai per renderitzar Retorna undefined en silenci

Regla mnemotècnica: parèntesis retornen, claus executen.

  1. El refactor: LlistaBicicletes amb map

Aquest és el canvi més important del mòdul. Compara l'abans i el després.

Abans

// src/components/LlistaBicicletes.jsx  — ABANS
import TargetaBicicleta from './TargetaBicicleta.jsx';

/**
 * Props: primera, segona, tercera (objectes bicicleta)
 */
function LlistaBicicletes({ primera, segona, tercera, alSeleccionar, alReservar }) {
  return (
    <section className="llista-bicicletes">
      <h2>Bicicletes del catàleg</h2>
      {primera && (
        <TargetaBicicleta bicicleta={primera} nomEstacio="Plaça Major"
          alSeleccionar={alSeleccionar} alReservar={alReservar} />
      )}
      {segona && (
        <TargetaBicicleta bicicleta={segona} nomEstacio="Plaça Major"
          alSeleccionar={alSeleccionar} alReservar={alReservar} />
      )}
      {tercera && (
        <TargetaBicicleta bicicleta={tercera} nomEstacio="Parc Nord"
          alSeleccionar={alSeleccionar} alReservar={alReservar} />
      )}
    </section>
  );
}

export default LlistaBicicletes;

Després

// src/components/LlistaBicicletes.jsx  — DESPRÉS
import TargetaBicicleta from './TargetaBicicleta.jsx';
import estils from './LlistaBicicletes.module.css';

/**
 * Secció del catàleg de CicloUrbano.
 * Props:
 *  - bicicletes (array d'objectes bicicleta, opcional, per defecte [])
 *  - estacions (array d'objectes estació, opcional, per defecte [])
 *  - alSeleccionar (funció, opcional): rep la bicicleta premuda
 *  - alReservar (funció, opcional): rep la bicicleta a reservar
 */
function LlistaBicicletes({ bicicletes = [], estacions = [], alSeleccionar, alReservar }) {
  // Índex auxiliar: id d'estació -> nom, per no recórrer l'array a cada targeta
  const nomPerEstacio = {};
  estacions.forEach((estacio) => {
    nomPerEstacio[estacio.id] = estacio.nom;
  });

  if (bicicletes.length === 0) {
    return (
      <section className={estils.llista}>
        <h2>Bicicletes del catàleg</h2>
        <p className={estils.buit}>
          No hi ha bicicletes que coincideixin amb el filtre. Prova amb un altre tipus.
        </p>
      </section>
    );
  }

  return (
    <section className={estils.llista}>
      <h2>Bicicletes del catàleg</h2>
      <p className={estils.recompte}>
        {bicicletes.length === 1
          ? "S'ha trobat 1 bicicleta."
          : `S'han trobat ${bicicletes.length} bicicletes.`}
      </p>

      {bicicletes.map((bicicleta) => (
        <TargetaBicicleta
          key={bicicleta.id}
          bicicleta={bicicleta}
          nomEstacio={nomPerEstacio[bicicleta.estacioId] ?? 'Estació desconeguda'}
          alSeleccionar={alSeleccionar}
          alReservar={alReservar}
        />
      ))}
    </section>
  );
}

export default LlistaBicicletes;

I a App.jsx desapareixen les tres props numerades:

// src/App.jsx (fragment)
import { bicicletes, estacions } from './dades/domini.js';

<LlistaBicicletes
  bicicletes={bicicletes}
  estacions={estacions}
  alSeleccionar={gestionarSeleccioBicicleta}
  alReservar={gestionarReserva}
/>

El que ha canviat, punt per punt:

  • Una prop en lloc de tres, i amb un nom que descriu què és la dada, no on és.
  • Les cinc bicicletes es veuen, no només les tres primeres. I si demà n'entren vint, no cal tocar res.
  • nomEstacio ja no està escrit a mà. Abans deia «Plaça Major» per a les dues primeres perquè casualment era cert; ara es resol a partir de bicicleta.estacioId, que és la dada real. Amb l'índex nomPerEstacio, bici-003 mostra correctament «Parc Nord» i bici-004, «Estació Central».
  • L'estat buit es comprova amb bicicletes.length === 0 mitjançant un retorn anticipat, tal com vas aprendre a 03-02.
  • key={bicicleta.id}: cada targeta porta la identitat estable que React necessita. Anem a per això.

  1. Què és key i per què React la necessita

A la lliçó 01-05 vas veure que React compara els dos arbres per posició, nivell a nivell, i que aquest criteri funciona bé excepte en un cas: les llistes que canvien. Aquí és on entra key.

Una clau és una cadena o un número que li diu a React: «aquest element de la llista és aquesta dada concreta, sigui on sigui ara». React no la fa servir per a res visual —no arriba al DOM, no la pots llegir des del component— sinó exclusivament per emparellar elements entre dos renders.

El cas que ho demostra: inserir al principi

Suposa que el catàleg mostra tres bicicletes i n'arriba una de nova pel davant.

// Render anterior: [bici-001, bici-002, bici-003]
// Render nou:      [bici-005, bici-001, bici-002, bici-003]

Sense claus estables (comparant per posició), React raona així:

flowchart TD
    subgraph SIN["SENSE claus estables: compara per posició"]
        direction TB
        A0["pos 0: bici-001 → bici-005<br/>ACTUALITZA tot el contingut"]
        A1["pos 1: bici-002 → bici-001<br/>ACTUALITZA tot el contingut"]
        A2["pos 2: bici-003 → bici-002<br/>ACTUALITZA tot el contingut"]
        A3["pos 3: (res) → bici-003<br/>CREA un node nou"]
    end

Quatre operacions sobre el DOM, i les tres primeres reescriuen targetes que no havien canviat.

Amb key={bicicleta.id}, React compara per identitat:

flowchart TD
    subgraph CON["AMB key={bicicleta.id}: compara per identitat"]
        direction TB
        B0["bici-001 ja existia → REUTILITZA, només es mou"]
        B1["bici-002 ja existia → REUTILITZA, només es mou"]
        B2["bici-003 ja existia → REUTILITZA, només es mou"]
        B3["bici-005 és nova → CREA i INSEREIX al principi"]
    end

Una sola creació i cap reescriptura. Però el benefici de debò no és el rendiment: és la correcció. Cada targeta conserva el seu estat intern —un desplegable obert, un camp d'hores a mig omplir, una marca de favorita— perquè React sap que continua sent la mateixa targeta.

Les regles de key

Regla Explicació
Única entre germans Dos elements de la mateixa llista no poden compartir clau. En una altra llista diferent sí que es pot repetir
Estable entre renders La mateixa dada ha de tenir sempre la mateixa clau. Res de Math.random() ni Date.now()
Va a l'element més extern del map Si embolcalles la targeta en un <li>, la clau va al <li>, no a la targeta
No és una prop key la consumeix React. Dins del component, props.key és undefined. Si necessites l'id, passa'l a part

Aquest darrer punt sorprèn molta gent:

{/* Si el component necessita l'id, cal passar-lo dues vegades */}
<TargetaBicicleta key={bicicleta.id} bicicleta={bicicleta} />
{/* A dins, es llegeix com a bicicleta.id — mai com a props.key */}

Si t'oblides de la clau, React no trenca res, però deixa un avís molt visible a la consola: «Warning: Each child in a list should have a unique "key" prop». No l'ignoris: significa que la teva llista es compara per posició.

  1. Què serveix com a clau i què no

Font de la clau Serveix? Per què
bicicleta.id (id del domini) Sí, la millor Únic, estable i ja existeix a les dades
Un identificador de base de dades El mateix motiu
Un camp únic de negoci (correu, matrícula) Sí, amb compte Ha de ser realment únic i immutable
Una combinació de camps: `${estacioId}-${bicicletaId}` Útil quan no hi ha un id únic propi
Un identificador generat en crear la dada (crypto.randomUUID()) Es genera una vegada i es guarda a la dada, no al render
bicicleta.model Gairebé mai Es repeteix: bici-001 i bici-004 són totes dues «Urbana Clàssica»
L'índex de l'array Gairebé mai És la posició, no la identitat. Vegeu l'apartat 7
Math.random() Mai Canvia a cada render: React destrueix i recrea tota la llista cada vegada
Date.now() Mai El mateix problema
Un comptador que incrementes al render Mai És l'índex disfressat, i a més muta durant el render

Sobre l'índex hi ha un matís honest: és acceptable si es compleixen les tres condicions alhora.

  1. La llista mai es reordena ni es filtra.
  2. Els elements mai s'insereixen ni s'eliminen pel mig o pel principi (només s'afegeixen al final, si de cas).
  3. Els elements no tenen estat intern ni contenen camps de formulari.

El SelectorTipus de 02-04 feia servir key={tipus} sobre una constant TIPUS que mai canvia: allà fins i tot l'índex hauria estat inofensiu. Però com que gairebé cap llista real compleix les tres condicions per sempre, la recomanació pràctica és: si la dada té id, fes servir l'id; si no en té, dóna-li'n un.

  1. L'índex de l'array: l'error reproduïble

La teoria no convenç tant com veure la fallada. Anem a construir-la.

// src/components/FilaFavorita.jsx

import { useState } from 'react';

/**
 * Fila d'una bicicleta amb una marca de favorita en ESTAT LOCAL.
 * Props:
 *  - bicicleta (objecte, obligatori)
 *  - alEliminar (funció, obligatòria): rep l'id de la bicicleta
 */
function FilaFavorita({ bicicleta, alEliminar }) {
  const [favorita, setFavorita] = useState(false);

  return (
    <li>
      <button type="button" onClick={() => setFavorita(!favorita)}>
        {favorita ? '★' : '☆'}
      </button>{' '}
      {bicicleta.model} ({bicicleta.id}){' '}
      <button type="button" onClick={() => alEliminar(bicicleta.id)}>
        Eliminar
      </button>
    </li>
  );
}

export default FilaFavorita;

I el component que la fa servir, amb la clau mal posada:

// src/components/LlistaFavorites.jsx  — VERSIÓ AMB LA FALLADA
import { useState } from 'react';
import { bicicletes as bicicletesInicials } from '../dades/domini.js';
import FilaFavorita from './FilaFavorita.jsx';

function LlistaFavorites() {
  const [llista, setLlista] = useState(bicicletesInicials);

  function gestionarEliminar(id) {
    setLlista(llista.filter((bicicleta) => bicicleta.id !== id));
  }

  return (
    <ul>
      {llista.map((bicicleta, index) => (
        // ✘ LA FALLADA: la clau és la posició, no la identitat
        <FilaFavorita key={index} bicicleta={bicicleta} alEliminar={gestionarEliminar} />
      ))}
    </ul>
  );
}

export default LlistaFavorites;

Com reproduir la fallada, pas a pas

  1. Marca com a favorita només bici-003 (Càrrega Max), la tercera de la llista. La seva estrella es posa en ★.
  2. Prem «Eliminar» a bici-001 (la primera).
  3. Observa la llista resultant.

El que esperes: bici-002, bici-003 (★), bici-004, bici-005. El que passa: bici-002, bici-003, bici-004 (★), bici-005. L'estrella s'ha mogut a la bicicleta equivocada.

Per què? Perquè l'estat favorita viu al component, i React associa cada component a la seva clau. La bicicleta favorita era a la posició 2, així que el seu estat va quedar associat a la clau 2. En eliminar la primera, tot es desplaça i ara a la posició 2 hi ha bici-004… que hereta l'estat de qui ocupava aquella clau abans.

Abans d'eliminar Després d'eliminar (clau = índex)
clau 0 → bici-001, favorita: no clau 0 → bici-002, favorita: no
clau 1 → bici-002, favorita: no clau 1 → bici-003, favorita: no ✘
clau 2 → bici-003, favorita: sí clau 2 → bici-004, favorita: sí
clau 3 → bici-004, favorita: no clau 3 → bici-005, favorita: no
clau 4 → bici-005, favorita: no

L'estat no es va moure: es va quedar enganxat a la posició, i les dades van lliscar per sota.

La correcció

{llista.map((bicicleta) => (
  <FilaFavorita key={bicicleta.id} bicicleta={bicicleta} alEliminar={gestionarEliminar} />
))}

Repeteix l'experiment: l'estrella es queda a bici-003 passi el que passi. Amb la clau correcta, React entén que bici-001 ha desaparegut i que les altres són les mateixes de sempre, així que conserva el seu estat i només elimina un node del DOM.

Aquest mateix mecanisme explica una fallada encara més freqüent en aplicacions reals: un camp de text a mig omplir que salta a una altra fila en ordenar o filtrar la llista. La causa és idèntica.

  1. Claus en fragments

A vegades cada element de la llista necessita produir diversos nodes germans sense un contenidor que els embolcalli. El cas típic és una llista de definició o una taula:

{estacions.map((estacio) => (
  <>
    <dt>{estacio.nom}</dt>
    <dd>{estacio.barri} · {estacio.places} places</dd>
  </>
))}

Això no funciona: la sintaxi curta <>…</> no admet atributs, així que no hi ha on posar la clau i React avisa. La solució és fer servir la forma llarga del fragment, important-lo des de React:

import { Fragment } from 'react';

function LlistaDefinicioEstacions({ estacions }) {
  return (
    <dl>
      {estacions.map((estacio) => (
        <Fragment key={estacio.id}>
          <dt>{estacio.nom}</dt>
          <dd>
            {estacio.barri} · {estacio.places} places
          </dd>
        </Fragment>
      ))}
    </dl>
  );
}

export default LlistaDefinicioEstacions;
Forma Admet key? Quan fer-la servir
<>…</> No Agrupar elements fora d'una llista
<Fragment key={…}>…</Fragment> Cada iteració produeix diversos germans
<div key={…}>…</div> Només si el contenidor té sentit semàntic o d'estil

L'avantatge del fragment davant del <div> és que no afegeix cap node al DOM. En una <dl>, una <table> o un contenidor amb display: grid, ficar-hi un <div> intermedi trencaria la semàntica o la maquetació.

  1. filter i sort sense mutar l'array original

Tan bon punt una llista es pinta amb map, el natural és voler-la filtrar i ordenar. Aquí hi ha una trampa de JavaScript que mossega fort a React.

filter és segur; sort no

const disponibles = bicicletes.filter((b) => b.estat === 'disponible');
// bicicletes NO s'ha modificat: filter retorna un array nou

bicicletes.sort((a, b) => a.preuHora - b.preuHora);
// ✘ bicicletes SÍ s'ha modificat: sort ordena AL MATEIX LLOC

Recorda la regla d'immutabilitat de la lliçó 02-04: mai modifiquis directament unes dades que vénen de props, de l'estat o d'un mòdul importat. Si fas sort sobre l'array del domini.js, el reordenes per a tota l'aplicació, sense que React se n'assabenti i sense poder tornar enrere.

Les tres formes correctes d'ordenar:

// 1. toSorted: mètode modern que retorna un array NOU ordenat (ES2023)
const perPreu = bicicletes.toSorted((a, b) => a.preuHora - b.preuHora);

// 2. Còpia prèvia amb spread i després sort sobre la còpia
const perPreu = [...bicicletes].sort((a, b) => a.preuHora - b.preuHora);

// 3. Còpia amb slice(), equivalent i compatible amb navegadors molt antics
const perPreu = bicicletes.slice().sort((a, b) => a.preuHora - b.preuHora);

toSorted, toReversed, toSpliced i with són la família de mètodes no destructius afegida a JavaScript el 2023. Estan disponibles a tots els navegadors actuals i són l'opció més llegible. Si el teu projecte ha de suportar entorns antics, la còpia amb spread fa exactament el mateix.

Mètode Muta l'original? Alternativa segura
map No
filter No
slice No
concat No
sort toSorted o [...array].sort()
reverse toReversed o [...array].reverse()
splice toSpliced o filter
push / pop / shift Spread: [...array, nou]

La cadena completa: filtrar, ordenar i pintar

// src/components/CatalegOrdenat.jsx
import TargetaBicicleta from './TargetaBicicleta.jsx';

/**
 * Catàleg filtrat per tipus i ordenat per preu ascendent.
 * Props:
 *  - bicicletes (array, obligatori)
 *  - tipus (cadena, opcional, per defecte 'todos')
 */
function CatalegOrdenat({ bicicletes, tipus = 'todos' }) {
  // Tota la cadena produeix arrays nous: l'original queda intacte
  const visibles = bicicletes
    .filter((bicicleta) => tipus === 'todos' || bicicleta.tipus === tipus)
    .toSorted((a, b) => a.preuHora - b.preuHora);

  if (visibles.length === 0) {
    return <p>No hi ha bicicletes de tipus «{tipus}».</p>;
  }

  return (
    <section>
      {visibles.map((bicicleta) => (
        <TargetaBicicleta key={bicicleta.id} bicicleta={bicicleta} />
      ))}
    </section>
  );
}

export default CatalegOrdenat;

Tres observacions importants:

  • visibles és un valor derivat, no estat. Es recalcula a cada render a partir de les props, i per això mai es pot desincronitzar. És exactament la lliçó de 02-04 aplicada a llistes.
  • La cadena es llegeix de dalt a baix: filtrar i després ordenar. Filtrar primer és a més més eficient, perquè s'ordenen menys elements.
  • Les claus continuen sent els id. Encara que l'ordre canviï amb cada criteri, cada targeta conserva la seva identitat i el seu estat intern.

Aquest component és l'assaig general del que passarà a Elevar l'Estat, quan el tipus deixi de ser una prop fixa i vingui del SelectorTipus.

  1. Llistes imbricades: estacions amb les seves bicicletes

Les llistes dins de llistes són habitualíssimes, i només tenen una regla nova: cada map necessita les seves pròpies claus, úniques entre els seus germans. No hi ha cap requisit d'unicitat global.

// src/components/EstacionsAmbFlota.jsx
import EtiquetaEstat from './EtiquetaEstat.jsx';
import estils from './EstacionsAmbFlota.module.css';

/**
 * Llistat d'estacions de CicloUrbano amb les bicicletes aparcades a cadascuna.
 * Props:
 *  - estacions (array, obligatori)
 *  - bicicletes (array, obligatori)
 */
function EstacionsAmbFlota({ estacions, bicicletes }) {
  return (
    <section className={estils.contenidor}>
      <h2>Estacions</h2>

      {estacions.map((estacio) => {
        // Llista derivada: les bicicletes D'AQUESTA estació
        const aEstacio = bicicletes.filter((b) => b.estacioId === estacio.id);
        const lliures = estacio.places - aEstacio.length;

        return (
          <article key={estacio.id} className={estils.estacio}>
            <h3>
              {estacio.nom} <small>({estacio.barri})</small>
            </h3>
            <p>
              {aEstacio.length} de {estacio.places} places ocupades · {lliures} lliures
            </p>

            {aEstacio.length > 0 ? (
              <ul className={estils.flota}>
                {aEstacio.map((bicicleta) => (
                  <li key={bicicleta.id}>
                    {bicicleta.model} <EtiquetaEstat estat={bicicleta.estat} />
                  </li>
                ))}
              </ul>
            ) : (
              <p className={estils.buit}>No hi ha bicicletes aparcades aquí.</p>
            )}
          </article>
        );
      })}
    </section>
  );
}

export default EstacionsAmbFlota;

Detalls que mereixen atenció:

  • El map exterior fa servir cos de bloc amb return, perquè necessita calcular aEstacio i lliures abans de retornar el JSX. És el cas 2 de l'apartat 3: si t'oblides d'aquest return, no es pinta cap estació.
  • Dos nivells de claus independents: key={estacio.id} a les estacions i key={bicicleta.id} a les bicicletes de cadascuna. Que bici-001 i est-01 no s'assemblin és irrellevant; només importa la unicitat entre germans.
  • aEstacio es calcula dins del map, així que cada estació té la seva pròpia llista derivada, sense estat addicional.

Amb les dades del domini.js, el resultat és:

Estació Places Bicicletes aparcades
est-01 Plaça Major (Centre) 20 bici-001 Urbana Clàssica, bici-002 Elèctrica Pro
est-02 Parc Nord (Nord) 15 bici-003 Càrrega Max, bici-005 Elèctrica Pro
est-03 Estació Central (Eixample) 30 bici-004 Urbana Clàssica
flowchart TD
    A["EstacionsAmbFlota"] --> B["map sobre estacions<br/>key = estacio.id"]
    B --> C1["est-01 Plaça Major"]
    B --> C2["est-02 Parc Nord"]
    B --> C3["est-03 Estació Central"]
    C1 --> D1["map sobre les seves bicicletes<br/>key = bicicleta.id"]
    C2 --> D2["map sobre les seves bicicletes<br/>key = bicicleta.id"]
    C3 --> D3["map sobre les seves bicicletes<br/>key = bicicleta.id"]

  1. L'estat buit d'una llista

Ja l'has fet servir als exemples, però convé fixar-lo com a patró perquè és dels que distingeixen una interfície acurada d'una descurada. Una llista pot estar buida per motius molt diferents, i el missatge hauria de dir quin:

Situació Missatge adequat
No hi ha dades en absolut «Encara no hi ha bicicletes registrades.»
Hi ha dades, però el filtre no troba res «Cap bicicleta coincideix amb el filtre. Prova amb un altre tipus.»
Les dades encara no han arribat «Carregant catàleg…»
Ha fallat la càrrega «No s'ha pogut carregar el catàleg. Torna-ho a intentar.»
function Cataleg({ bicicletes, tipusFiltre }) {
  const visibles = bicicletes.filter(
    (b) => tipusFiltre === 'todos' || b.tipus === tipusFiltre
  );

  if (bicicletes.length === 0) {
    return <p className="buit">Encara no hi ha bicicletes registrades.</p>;
  }

  if (visibles.length === 0) {
    return (
      <p className="buit">
        Cap bicicleta coincideix amb el filtre «{tipusFiltre}». Prova amb un altre tipus.
      </p>
    );
  }

  return (
    <section>
      {visibles.map((bicicleta) => (
        <TargetaBicicleta key={bicicleta.id} bicicleta={bicicleta} />
      ))}
    </section>
  );
}

Distingir «no hi ha res» de «no hi ha res que coincideixi» costa tres línies i evita que ningú pensi que l'aplicació està trencada. Els casos de càrrega i error es tractaran quan arribin les dades asíncrones, a Hook useEffect i Límits d'Error.

Errors Comuns i Consells

  • Oblidar el return dins d'un map amb claus. La llista surt buida i no hi ha cap error. Recorda: parèntesis retornen, claus executen.
  • Fer servir forEach en lloc de map. forEach sempre retorna undefined: no produeix res per pintar. Per generar elements, map.
  • Ometre la key. React avisa per consola i la teva llista passa a comparar-se per posició, amb tots els problemes de l'apartat 7.
  • Fer servir l'índex com a clau en una llista que es filtra, ordena o elimina. L'estat intern es queda enganxat a la posició i acaba a la fila equivocada.
  • Fer servir Math.random() com a clau. Cada render genera claus noves, així que React destrueix i recrea tota la llista: perds l'estat, el focus i el rendiment.
  • Posar la key a l'element equivocat. Ha d'anar al node més extern que retorna el map, no a un fill seu.
  • Intentar llegir props.key dins del component. No existeix. Si necessites l'identificador, passa'l també com a prop normal.
  • Repetir claus entre germans. Amb dos elements amb la mateixa clau, React avisa i el comportament passa a ser impredictible. Compte amb fer servir model com a clau: bici-001 i bici-004 comparteixen model.
  • Fer servir <>…</> quan cal una clau. La sintaxi curta no admet atributs; fes servir <Fragment key={…}>.
  • Ordenar amb sort sobre les props o l'estat. sort muta l'array. Fes servir toSorted o una còpia prèvia.
  • Consell: si les teves dades no tenen id, genera'l en crear-les, no en renderitzar-les. crypto.randomUUID() en el moment d'afegir l'element, guardat dins de l'objecte.
  • Consell: calcula la llista visible com a valor derivat, encadenant filter i toSorted just abans del return. No la guardis en estat: es desincronitzaria.
  • Consell: extreu el contingut de la iteració a un component propi tan aviat com passi de cinc o sis línies. {bicicletes.map(b => <TargetaBicicleta key={b.id} bicicleta={b} />)} és una línia que s'entén sola.

Exercicis

Exercici 1

Aquest component té quatre errors relacionats amb llistes i claus. Identifica'ls, explica el símptoma de cadascun i escriu la versió corregida.

function TaulaBicicletes({ bicicletes }) {
  const ordenades = bicicletes.sort((a, b) => a.preuHora - b.preuHora);

  return (
    <table>
      <tbody>
        {ordenades.map((bicicleta, i) => {
          const preu = bicicleta.preuHora.toFixed(2);
          <tr key={i}>
            <td>{bicicleta.model}</td>
            <td>{preu} €</td>
          </tr>;
        })}
      </tbody>
    </table>
  );
}

Exercici 2

Crea el component ResumPerTipus (src/components/ResumPerTipus.jsx) que rebi l'array bicicletes i mostri una taula amb una fila per cada tipus (urbana, electrica, carga), indicant quantes bicicletes hi ha d'aquest tipus, quantes estan disponibles i el preu mitjà per hora formatat a la catalana.

Requisits:

  • Reutilitza la constant TIPUS sense el valor 'todos'.
  • No mutis l'array rebut.
  • Si un tipus no té cap bicicleta, la fila ha de mostrar «—» al preu mitjà en lloc de NaN.
  • Fes servir claus adequades.

Exercici 3

Partint del component LlistaFavorites de l'apartat 7, amplia'l perquè a més permeti moure una bicicleta al principi de la llista amb un botó «Pujar al principi». Després:

  1. Executa'l amb key={index}, marca com a favorita bici-005 (la darrera) i puja-la al principi. Descriu què passa amb l'estrella i explica per què.
  2. Canvia'l a key={bicicleta.id} i repeteix-ho. Explica la diferència en termes de reconciliació.

Solucions

Solució 1.

Els quatre errors:

Error Símptoma
bicicletes.sort(...) sense copiar Muta l'array rebut per props, reordenant-lo per a tota l'aplicació sense avisar React
El map fa servir claus sense return La devolució és undefined per a cada fila: la taula surt completament buida, sense errors
key={i} L'índex com a clau: si la taula es reordena per un altre criteri, qualsevol estat intern es desplaça de fila
toFixed(2) sense format català Mostra 2.50 en lloc de 2,50. No és un error funcional, però trenca la convenció del projecte
// src/components/TaulaBicicletes.jsx

/**
 * Taula de bicicletes ordenada per preu ascendent.
 * Props:
 *  - bicicletes (array, opcional, per defecte [])
 */
function TaulaBicicletes({ bicicletes = [] }) {
  // toSorted retorna un array nou: l'original no es toca
  const ordenades = bicicletes.toSorted((a, b) => a.preuHora - b.preuHora);

  if (ordenades.length === 0) {
    return <p>No hi ha bicicletes per mostrar.</p>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>Model</th>
          <th>Preu per hora</th>
        </tr>
      </thead>
      <tbody>
        {ordenades.map((bicicleta) => {
          const preu = bicicleta.preuHora.toFixed(2).replace('.', ',');

          return (
            <tr key={bicicleta.id}>
              <td>{bicicleta.model}</td>
              <td>{preu} €</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

export default TaulaBicicletes;

Solució 2.

// src/components/ResumPerTipus.jsx

const TIPUS_BICICLETA = ['urbana', 'electrica', 'carga'];

const NOMS_TIPUS = {
  urbana: 'Urbanes',
  electrica: 'Elèctriques',
  carga: 'De càrrega'
};

/**
 * Resum de la flota agrupat per tipus de bicicleta.
 * Props:
 *  - bicicletes (array, opcional, per defecte [])
 *
 * Sense estat: totes les xifres són valors derivats de la prop.
 */
function ResumPerTipus({ bicicletes = [] }) {
  return (
    <table className="resum-per-tipus">
      <thead>
        <tr>
          <th>Tipus</th>
          <th>Total</th>
          <th>Disponibles</th>
          <th>Preu mitjà</th>
        </tr>
      </thead>
      <tbody>
        {TIPUS_BICICLETA.map((tipus) => {
          // filter no muta: cada crida retorna un array nou
          const delTipus = bicicletes.filter((bicicleta) => bicicleta.tipus === tipus);
          const disponibles = delTipus.filter((b) => b.estat === 'disponible').length;

          const suma = delTipus.reduce((total, b) => total + b.preuHora, 0);
          const mitjana =
            delTipus.length > 0
              ? (suma / delTipus.length).toFixed(2).replace('.', ',') + ' €'
              : '—';

          return (
            <tr key={tipus}>
              <td>{NOMS_TIPUS[tipus]}</td>
              <td>{delTipus.length}</td>
              <td>{disponibles}</td>
              <td>{mitjana}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

export default ResumPerTipus;

Amb les dades del domini.js el resultat és:

Tipus Total Disponibles Preu mitjà
Urbanes 2 2 2,50 €
Elèctriques 2 1 4,00 €
De càrrega 1 0 5,50 €

La clau és tipus perquè els tipus són únics entre ells i estables: l'array TIPUS_BICICLETA mai es reordena. El if del preu mitjà evita el NaN que produiria dividir entre zero.

Solució 3.

L'afegit al component:

function gestionarPujar(id) {
  const escollida = llista.find((bicicleta) => bicicleta.id === id);
  const resta = llista.filter((bicicleta) => bicicleta.id !== id);
  setLlista([escollida, ...resta]);   // array NOU: mai es muta l'anterior
}

1. Amb key={index}. Marques bici-005 (posició 4) com a favorita: l'estat favorita: true queda associat a la clau 4. En pujar-la al principi, bici-005 passa a la clau 0, i la clau 4 l'ocupa ara bici-004. React veu que a la clau 0 continua havent-hi un FilaFavorita del mateix tipus, així que reutilitza el component i el seu estat, i només actualitza les props. Resultat: l'estrella es queda a la posició 4, és a dir, sobre bici-004, mentre que bici-005, ja al principi, apareix sense marcar. L'estat ha viatjat a la bicicleta equivocada.

2. Amb key={bicicleta.id}. React emparella els elements per identitat, no per posició: reconeix que bici-005 continua existint —simplement s'ha mogut— i mou el seu node del DOM juntament amb el seu estat, sense recrear-lo. Els altres components ni tan sols es toquen. Resultat: l'estrella acompanya bici-005 fins al principi de la llista, que és el que qualsevol esperaria.

La diferència, en termes de reconciliació: amb l'índex, React emparella posició amb posició, així que un moviment s'interpreta com «tots aquests elements han canviat de contingut». Amb l'id, emparella identitat amb identitat, així que un moviment s'interpreta com el que és: un moviment.

Conclusió

Amb aquesta lliçó es tanquen els dos deutes que arrossegava el curs. El primer era pràctic: LlistaBicicletes ja no rep primera, segona i tercera ni escriu les targetes a mà, sinó que rep l'array complet i el recorre amb map, resolent a més el nom de cada estació a partir de estacioId en lloc de tenir-lo escrit a mà. Les cinc bicicletes del domini.js es veuen, i si demà n'hi ha cinquanta no caldrà tocar ni una línia. El segon deute venia de la lliçó 01-05: ara saps què fa exactament una key —emparellar elements entre renders per identitat en lloc de per posició— i has vist la fallada que evita, amb una marca de favorit que es queda a la fila equivocada en eliminar un element.

Pel camí has après la mecànica completa: que map transforma un array de dades en un array d'elements i que React sap pintar arrays; que les claus de bloc exigeixen return i els parèntesis no; que la clau ha de ser única entre germans i estable entre renders, que l'id del domini és gairebé sempre la resposta correcta i que l'índex, Math.random() i els camps repetits no ho són; que els fragments amb clau s'escriuen <Fragment key={…}> perquè la sintaxi curta no admet atributs; que filter és segur i sort muta, per la qual cosa cal fer servir toSorted o una còpia prèvia; i que les llistes imbricades només requereixen claus úniques entre germans, no globalment.

El catàleg de CicloUrbano està complet com a aparador interactiu: es pinta a partir de les dades, reacciona als clics, s'adapta a l'estat de cada bicicleta i sap què dir quan no hi ha res a mostrar. El que encara no pot fer l'aplicació és recollir informació. Una reserva necessita triar bicicleta, indicar una data, unes hores i acceptar les condicions; i per a això calen camps de formulari el valor dels quals estigui sota el control de React, no del DOM. Aquest és el pas següent: Formularis i Components Controlats.

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