CicloUrbano ja respon als clics, però pinta sempre el mateix: la targeta d'una bicicleta en manteniment ofereix un botó «Reservar» que no hauria de ser-hi, i un catàleg sense resultats es queda en blanc sense explicar per què. La interfície ha de decidir, i decidir és exactament el que fa el renderitzat condicional. A React no existeix cap sintaxi especial per a això: com que el JSX és JavaScript, es fan servir les eines del llenguatge —if, l'operador ternari, &&, ??, objectes i switch—, cadascuna amb el seu punt fort i el seu punt de trencament. En aquesta lliçó veuràs les sis tècniques aplicades al mateix cas, per poder-les comparar de debò, aprendràs quan convé cadascuna i desactivaràs una trampa clàssica de l'operador && que pot colar un 0 solt enmig de la teva interfície.

Contingut

  1. El principi: interfície = f(estat)
  2. Tècnica 1: if/else abans del return
  3. Tècnica 2: l'operador ternari dins del JSX
  4. Tècnica 3: && per mostrar o no mostrar
  5. Tècnica 4: ?? per a valors absents
  6. Tècnica 5: objecte de correspondències
  7. Tècnica 6: switch en una funció auxiliar
  8. Taula comparativa: quan fer servir cadascuna
  9. Retorn anticipat: no pintar res
  10. La trampa del && amb números i cadenes
  11. Components condicionals amb nom
  12. CicloUrbano: targetes que s'adapten a l'estat

  1. El principi: interfície = f(estat)

A la primera lliçó del curs vam definir React amb una fórmula: la interfície és una funció de les dades. El renderitzat condicional és la conseqüència directa d'aquesta idea. No manipules el DOM per amagar un botó; descrius què ha d'aparèixer per a cada combinació de dades i deixes que React calculi la diferència.

Compara-ho amb el que faries sense React:

// JavaScript sense framework: dones ordres al DOM
const boto = document.querySelector('.boto-reservar');
if (bicicleta.estat === 'disponible') {
  boto.style.display = 'block';
} else {
  boto.style.display = 'none';
}
// React: descrius el resultat, no els passos
{bicicleta.estat === 'disponible' && <button>Reservar</button>}

La diferència no és només de longitud. A la versió imperativa el botó existeix sempre al DOM i tu t'encarregues d'amagar-lo; si demà afegeixes un tercer estat, cal recordar-se de tocar aquest codi. A la versió declarativa el botó no existeix quan no ha d'existir, i la condició està escrita just on es llegeix el marcatge.

I una idea que convé fixar des d'ara: a React, no pintar alguna cosa s'expressa retornant un valor que no es pinta. Com vas veure a la lliçó 01-04, null, undefined, false i true no produeixen res en pantalla. Totes les tècniques d'aquesta lliçó es recolzen en aquesta propietat.

  1. Tècnica 1: if/else abans del return

La forma més llegible quan la condició afecta una part gran del resultat: es decideix abans, es guarda en una variable i el JSX queda net.

// src/components/AvisEstat.jsx

/**
 * Missatge explicatiu de l'estat d'una bicicleta.
 * Props:
 *  - bicicleta (objecte, obligatori)
 */
function AvisEstat({ bicicleta }) {
  let missatge;

  if (bicicleta.estat === 'disponible') {
    missatge = <p className="avis avis--ok">Llesta per sortir.</p>;
  } else if (bicicleta.estat === 'alquilada') {
    missatge = <p className="avis avis--espera">En ús; torna a consultar més tard.</p>;
  } else {
    missatge = <p className="avis avis--taller">Al taller: no es pot reservar.</p>;
  }

  return (
    <div className="avis-estat">
      <h4>{bicicleta.model}</h4>
      {missatge}
    </div>
  );
}

export default AvisEstat;

Punts importants:

  • let missatge guarda un element JSX. Recorda de la lliçó 01-04 que un element JSX és un objecte JavaScript normal: es pot guardar en una variable, ficar en un array o passar com a prop.
  • L'if va fora del return. Dins del JSX només caben expressions, i if és una sentència. Escriure {if (x) ...} dins de les claus és un error de sintaxi.
  • El JSX final es llegeix d'una ullada. Tota la lògica de decisió és a dalt, agrupada, i el marcatge a baix.

Aquesta tècnica és la millor quan les branques són llargues o quan cal calcular altres coses pel camí (un text, una classe, una dada derivada).

  1. Tècnica 2: l'operador ternari dins del JSX

L'operador ternari condicio ? valorSiCert : valorSiFals sí que és una expressió, així que pot anar dins de les claus. És l'eina per triar entre dues alternatives concretes.

function AvisDisponibilitat({ bicicleta }) {
  return (
    <p className="avis">
      {bicicleta.estat === 'disponible'
        ? 'Llesta per sortir.'
        : 'No disponible ara mateix.'}
    </p>
  );
}

Funciona igual amb elements complets, no només amb text:

<div className="accions">
  {bicicleta.estat === 'disponible' ? (
    <button type="button">Reservar</button>
  ) : (
    <span className="no-disponible">No reservable</span>
  )}
</div>

Fixa't en els parèntesis que envolten cada branca quan el JSX ocupa diverses línies: no són obligatoris sintàcticament, però eviten errors del punt i coma automàtic i fan el bloc molt més llegible.

També es fa servir constantment per triar classes o atributs, no només contingut:

<article className={bicicleta.estat === 'disponible' ? 'targeta' : 'targeta targeta--atenuada'}>
<button type="button" disabled={bicicleta.estat !== 'disponible'}>Reservar</button>

El seu límit és l'imbricament. Un ternari dins d'un altre encara es llegeix; tres ja no:

{/* Llegible, al límit */}
{estat === 'disponible' ? 'Lliure' : estat === 'alquilada' ? 'En ús' : 'Al taller'}

{/* Il·legible: això és el que cal evitar */}
{estat === 'disponible'
  ? (rol === 'operario' ? <BotoTaller /> : <BotoReservar />)
  : (estat === 'alquilada' ? <Avis text="En ús" /> : <Avis text="Al taller" />)}

Quan arribis a aquest punt, la resposta no és «formatar millor»: és canviar de tècnica (apartats 6, 7 i 11).

  1. Tècnica 3: && per mostrar o no mostrar

Quan l'alternativa és «això o res», el ternari amb : null sobra i l'operador && és més directe.

{bicicleta.estat === 'disponible' && <button type="button">Reservar</button>}

Com funciona exactament, perquè no és màgia de React sinó JavaScript pur:

  1. && avalua l'operand de l'esquerra.
  2. Si és falsy, retorna aquest operand i no avalua el de la dreta.
  3. Si és truthy, retorna l'operand de la dreta.

Així que quan la condició és false, l'expressió completa val false, i React no pinta els booleans. Quan és true, l'expressió val l'element JSX, i React el pinta.

Casos típics:

{/* Un avís que només apareix quan cal */}
{bicicleta.estat === 'mantenimiento' && (
  <p className="avis-taller">Aquesta bicicleta està en revisió.</p>
)}

{/* Un comptador que només apareix si hi ha alguna cosa a comptar */}
{bicicletesDisponibles > 0 && <span>{bicicletesDisponibles} disponibles</span>}

{/* Un bloc d'administració només per a operaris */}
{usuari.rol === 'operario' && <PanellOperari usuari={usuari} />}

És la tècnica més utilitzada de totes, i també la que més maldecaps dona per un motiu molt concret que veuràs a l'apartat 10. Guarda ja la regla: a l'esquerra de && hi ha d'haver sempre un booleà.

  1. Tècnica 4: ?? per a valors absents

L'operador de coalescència nul·la (??) retorna l'operand de la dreta només quan el de l'esquerra és null o undefined. És el complement perfecte per a dades opcionals.

function FitxaEstacio({ estacio, notes }) {
  return (
    <div className="fitxa-estacio">
      <h3>{estacio.nom}</h3>
      <p>Barri: {estacio.barri ?? 'Sense assignar'}</p>
      <p>Notes: {notes ?? 'Sense notes de l\'operari.'}</p>
    </div>
  );
}

La diferència amb || és subtil però decisiva, i en una aplicació amb números i textos apareix constantment:

Valor de l'esquerra Amb || Amb ??
undefined fa servir el de la dreta fa servir el de la dreta
null fa servir el de la dreta fa servir el de la dreta
0 fa servir el de la dreta (malament) fa servir el 0
'' (cadena buida) fa servir el de la dreta fa servir la cadena buida
false fa servir el de la dreta fa servir false
'Centre' fa servir 'Centre' fa servir 'Centre'

L'exemple que ho deixa clar amb dades de CicloUrbano:

const placesLliures = 0;

<p>Places lliures: {placesLliures || 'desconegudes'}</p>   // -> "desconegudes"  ✘  són 0!
<p>Places lliures: {placesLliures ?? 'desconegudes'}</p>   // -> "0"             ✔

Regla pràctica: fes servir ?? per a valors que poden faltar i || només quan de debò vulguis tractar el 0 i la cadena buida com «absents».

  1. Tècnica 5: objecte de correspondències

Quan hi ha tres o més casos enumerables, la solució més neta no és una cadena d'if ni un ternari imbricat: és un objecte que associa cada valor amb el que cal pintar.

// src/components/EtiquetaEstat.jsx
import { classes } from '../utilitats/classes.js';
import estils from './EtiquetaEstat.module.css';

// El mapa viu FORA del component: és una constant, no canvia entre renders
const ESTATS = {
  disponible: { text: 'Disponible', clau: 'disponible' },
  alquilada: { text: 'Llogada', clau: 'alquilada' },
  mantenimiento: { text: 'Al taller', clau: 'mantenimiento' }
};

const ESTAT_DESCONEGUT = { text: 'Estat desconegut', clau: 'desconegut' };

/**
 * Distintiu visual de l'estat d'una bicicleta.
 * Props:
 *  - estat (cadena, opcional, per defecte 'disponible'):
 *    'disponible' | 'alquilada' | 'mantenimiento'
 */
function EtiquetaEstat({ estat = 'disponible' }) {
  const dades = ESTATS[estat] ?? ESTAT_DESCONEGUT;

  return (
    <span className={classes(estils.etiqueta, estils[dades.clau])}>{dades.text}</span>
  );
}

export default EtiquetaEstat;

Què hi guanya aquesta versió respecte a la de 02-05:

  • El text visible deixa de ser el valor tècnic. Abans pintava literalment mantenimiento; ara pinta «Al taller», que és el que entén una persona usuària.
  • Afegir un quart estat és afegir una línia a l'objecte. No es toca el JSX ni la lògica.
  • ?? ESTAT_DESCONEGUT cobreix el cas imprevist. Si algun dia arriba un estat no contemplat, l'etiqueta ho diu en lloc de quedar-se buida o trencar-se.
  • El mapa és fora del component. Declarar-lo a dins el recrearia a cada render sense necessitat, i a més deixa clar que és una constant del domini, no una dada del component.

El mateix patró serveix per triar components sencers, no només textos:

import AvisDisponible from './AvisDisponible.jsx';
import AvisLlogada from './AvisLlogada.jsx';
import AvisTaller from './AvisTaller.jsx';

const AVISOS = {
  disponible: AvisDisponible,
  alquilada: AvisLlogada,
  mantenimiento: AvisTaller
};

function AvisDEstat({ bicicleta }) {
  // Compte: la variable ha de començar per MAJÚSCULA perquè JSX la tracti com a component
  const Avis = AVISOS[bicicleta.estat];

  if (!Avis) {
    return null;
  }

  return <Avis bicicleta={bicicleta} />;
}

Aquí apareix la regla de la majúscula de la lliçó 01-03 en la seva forma més subtil: const Avis = AVISOS[...] s'ha de dir Avis i no avis, perquè JSX interpreta les etiquetes en minúscula com a elements del DOM. <avis /> intentaria crear una etiqueta HTML inexistent.

  1. Tècnica 6: switch en una funció auxiliar

El switch és una sentència, així que no cap al JSX. Però sí que cap en una funció auxiliar que retorna JSX i es crida des del marcatge.

function MissatgeReserva({ reserva }) {
  function renderitzarMissatge() {
    switch (reserva.estat) {
      case 'activa':
        return <p className="msg msg--ok">Reserva activa. Recull la bicicleta a temps.</p>;
      case 'finalizada':
        return <p className="msg">Reserva finalitzada. Gràcies per utilitzar CicloUrbano.</p>;
      case 'cancelada':
        return <p className="msg msg--aviso">Reserva cancel·lada. No s'ha fet cap càrrec.</p>;
      default:
        return <p className="msg">Estat de reserva no reconegut.</p>;
    }
  }

  return (
    <section className="missatge-reserva">
      <h4>Reserva {reserva.id}</h4>
      {renderitzarMissatge()}
    </section>
  );
}

Dues observacions:

  • La funció es crida amb parèntesis ({renderitzarMissatge()}), perquè volem el seu resultat ara, no una referència. És el contrari del que fèies amb els gestors d'esdeveniments a 03-01: allà volies la funció, aquí vols el JSX que retorna.
  • El default no és opcional a la pràctica. Sense ell, un valor inesperat fa que la funció retorni undefined i no es pinti res, un error silenciós difícil de detectar.

Quan switch en lloc d'objecte de correspondències? Quan cada branca necessita lògica, no només un valor: calcular alguna cosa, compondre diversos elements, consultar una altra prop.

  1. Taula comparativa: quan fer servir cadascuna

Tècnica Núm. de casos Va dins del JSX Llegibilitat en imbricar Quan fer-la servir
if/else 2 o més No, abans del return Bona Branques llargues, o quan cal calcular més coses pel camí
Ternari Exactament 2 Dolenta a partir del segon nivell Triar entre dos textos, dos elements, dues classes
&& 1 (mostrar o res) Bona si la condició és senzilla Avisos, distintius, blocs opcionals
?? Valor o alternativa Bona Dades que poden ser null o undefined
Objecte de correspondències 3 o més Sí (consulta) Excel·lent: és pla Estats enumerables amb text, classe o component associat
switch auxiliar 3 o més Sí (crida) Bona Casos enumerables on cada branca té lògica pròpia
Retorn anticipat 1 (tot o res) No Excel·lent Dades que falten, permisos, estats de càrrega

La progressió natural en escriure codi real: comences amb &&, passes a ternari quan apareix la segona alternativa i saltes a un objecte o a components amb nom quan arribes al tercer cas. El símptoma que t'has passat de tècnica és haver de comptar parèntesis per entendre el teu propi JSX.

  1. Retorn anticipat: no pintar res

Un component pot decidir que no s'ha de pintar en absolut. La forma idiomàtica és retornar null com més aviat millor.

// src/components/AvisManteniment.jsx

/**
 * Avís que només apareix si la bicicleta està al taller.
 * Props:
 *  - bicicleta (objecte, obligatori)
 */
function AvisManteniment({ bicicleta }) {
  // Retorn anticipat: si no hi ha res a avisar, el component no pinta res
  if (bicicleta.estat !== 'mantenimiento') {
    return null;
  }

  return (
    <p className="avis-taller" role="status">
      La bicicleta {bicicleta.model} està en revisió i no es pot reservar.
    </p>
  );
}

export default AvisManteniment;

Detalls que convé entendre:

  • return null no és un error: és un valor vàlid de retorn que significa «no produeixo marcatge».
  • El component es continua muntant i executant. Retornar null no el desmunta: React el manté a l'arbre, amb el seu estat intacte, simplement sense nodes al DOM. Aquesta distinció importarà quan arribis als efectes al Mòdul 5.
  • També serveix per protegir-se de dades que falten, i aquest és el seu ús més freqüent:
function PanellReserva({ bicicleta }) {
  if (!bicicleta) {
    return <p>Selecciona una bicicleta per reservar.</p>;
  }

  // A partir d'aquí sabem que "bicicleta" existeix: la resta del codi va sense por
  return <div className="panell-reserva">…</div>;
}

Aquest patró, anomenat clàusula de guarda, evita que tot el cos del component quedi embolcallat en un if gegant i elimina d'un cop els errors del tipus «Cannot read properties of undefined».

  1. La trampa del && amb números i cadenes

A la lliçó 01-04 va aparèixer el «zero fantasma» amb placesLliures. Ara que && és la teva eina principal, toca veure'l a l'escenari on més mossega: comptant elements d'una col·lecció.

// MALAMENT: quan el catàleg és buit, en pantalla apareix un "0" solt
function Cataleg({ bicicletas }) {
  return (
    <section>
      <h2>Catàleg</h2>
      {bicicletas.length && <p>S'han trobat {bicicletas.length} bicicletes.</p>}
    </section>
  );
}

Pas a pas, amb bicicletas = []:

  1. bicicletas.length val 0.
  2. 0 és falsy, així que && retorna 0 sense avaluar el de la dreta.
  3. L'expressió completa val 0… i 0 és un número que React sí que pinta.
  4. Resultat: al buit on no hi hauria d'haver res apareix un 0 desconcertant.

És exactament el que passa amb false, excepte que false no es pinta i 0 sí. Les tres solucions correctes:

{/* 1. Comparació explícita: l'esquerra és un booleà de veritat. LA RECOMANADA */}
{bicicletas.length > 0 && <p>S'han trobat {bicicletas.length} bicicletes.</p>}

{/* 2. Ternari, que a més deixa escrit el cas contrari */}
{bicicletas.length > 0
  ? <p>S'han trobat {bicicletas.length} bicicletes.</p>
  : <p>No hi ha bicicletes per mostrar.</p>}

{/* 3. Doble negació: correcta, però comunica pitjor la intenció */}
{!!bicicletas.length && <p>S'han trobat {bicicletas.length} bicicletes.</p>}

El mateix problema, amb matisos, apareix amb cadenes de text:

const nota = '';

{nota && <p className="nota">{nota}</p>}

Aquí la cadena buida és falsy, l'expressió val '' i React no pinta les cadenes buides, així que visualment no passa res dolent. Però el codi continua sent fràgil: si demà aquesta variable pot valer '0' o un número, l'error apareix. Escriu sempre la condició completa:

{nota !== '' && <p className="nota">{nota}</p>}
{typeof nota === 'string' && nota.length > 0 && <p className="nota">{nota}</p>}
Valor a l'esquerra de && Què retorna l'expressió Què pinta React
false false Res ✔
null / undefined aquest valor Res ✔
0 0 0
NaN NaN NaN
'' '' Res (però és fràgil)
true l'element JSX L'element ✔

Regla definitiva: a l'esquerra de &&, sempre una comparació. x > 0, x !== '', Boolean(x), x === 'disponible'. Mai un valor a seques.

  1. Components condicionals amb nom

Existeix una tècnica que no és un operador, sinó una decisió de disseny: quan el condicional es complica, extreu cada branca a un component amb un nom que expliqui què és.

Compara-ho. Versió amb ternaris imbricats:

function AccionsBicicleta({ bicicleta, usuari }) {
  return (
    <div className="accions">
      {bicicleta.estat === 'disponible'
        ? (usuari.rol === 'operario'
            ? <button type="button">Enviar a taller</button>
            : <button type="button">Reservar</button>)
        : (bicicleta.estat === 'alquilada'
            ? <span>En ús fins a les 12:00</span>
            : <span>En revisió</span>)}
    </div>
  );
}

Ningú vol mantenir això. Versió amb components amb nom:

// src/components/AccionsBicicleta.jsx

function AccioReservar({ bicicleta, alReservar }) {
  return (
    <button type="button" onClick={() => alReservar(bicicleta)}>
      Reservar per {bicicleta.preuHora.toFixed(2).replace('.', ',')} €/hora
    </button>
  );
}

function AccioTaller({ bicicleta, alEnviarATaller }) {
  return (
    <button type="button" onClick={() => alEnviarATaller(bicicleta)}>
      Enviar a taller
    </button>
  );
}

function AvisNoDisponible({ bicicleta }) {
  const text = bicicleta.estat === 'alquilada' ? 'En ús ara mateix' : 'En revisió';
  return <span className="no-disponible">{text}</span>;
}

/**
 * Accions disponibles per a una bicicleta segons el seu estat i el rol de l'usuari.
 * Props:
 *  - bicicleta (objecte, obligatori)
 *  - usuari (objecte, obligatori) { id, nom, email, rol }
 *  - alReservar, alEnviarATaller (funcions, opcionals)
 */
function AccionsBicicleta({ bicicleta, usuari, alReservar, alEnviarATaller }) {
  if (bicicleta.estat !== 'disponible') {
    return (
      <div className="accions">
        <AvisNoDisponible bicicleta={bicicleta} />
      </div>
    );
  }

  return (
    <div className="accions">
      {usuari.rol === 'operario' ? (
        <AccioTaller bicicleta={bicicleta} alEnviarATaller={alEnviarATaller} />
      ) : (
        <AccioReservar bicicleta={bicicleta} alReservar={alReservar} />
      )}
    </div>
  );
}

export default AccionsBicicleta;

El que s'hi ha guanyat:

Abans Després
Un ternari de quatre branques imbricades Un retorn anticipat i un ternari pla
Els noms dels casos no existeixen AccioReservar, AccioTaller, AvisNoDisponible documenten el domini
Canviar un cas obliga a rellegir-ho tot Cada cas es llegeix i es prova per separat
Impossible reutilitzar una branca Qualsevol branca es pot fer servir en una altra pantalla

Aquest és el mateix criteri de límits que vas aprendre a la lliçó 02-01: si una branca del condicional té nom propi a la conversa de l'equip, mereix ser un component.

  1. CicloUrbano: targetes que s'adapten a l'estat

Apliquem ara el que hem après a l'aplicació. TargetaBicicleta passa a decidir tres coses: si mostra el botó «Reservar», si mostra l'avís de taller i com descriu el preu.

// src/components/TargetaBicicleta.jsx
import { classes } from '../utilitats/classes.js';
import estils from './TargetaBicicleta.module.css';
import EtiquetaEstat from './EtiquetaEstat.jsx';

/**
 * Targeta d'una bicicleta del catàleg de CicloUrbano.
 * Props:
 *  - bicicleta (objecte, obligatori) { id, model, tipus, estat, estacioId, preuHora }
 *  - nomEstacio (cadena, opcional, per defecte 'Estació desconeguda')
 *  - alSeleccionar (funció, opcional): rep la bicicleta en prémer la targeta
 *  - alReservar (funció, opcional): rep la bicicleta en prémer «Reservar»
 */
function TargetaBicicleta({
  bicicleta,
  nomEstacio = 'Estació desconeguda',
  alSeleccionar,
  alReservar
}) {
  // Valors derivats: es calculen a cada render, no són estat
  const disponible = bicicleta.estat === 'disponible';
  const enTaller = bicicleta.estat === 'mantenimiento';
  const preuFormatat = bicicleta.preuHora.toFixed(2).replace('.', ',');

  function gestionarClicTargeta() {
    if (alSeleccionar) {
      alSeleccionar(bicicleta);
    }
  }

  function gestionarClicReservar(esdeveniment) {
    esdeveniment.stopPropagation();
    if (alReservar) {
      alReservar(bicicleta);
    }
  }

  return (
    <article
      className={classes(estils.targeta, estils[bicicleta.tipus], !disponible && estils.atenuada)}
      onClick={gestionarClicTargeta}
    >
      <h3 className={estils.titol}>
        {bicicleta.model} <EtiquetaEstat estat={bicicleta.estat} />
      </h3>

      <p className={estils.dada}>Tipus: {bicicleta.tipus}</p>
      <p className={estils.dada}>Estació: {nomEstacio ?? 'Estació desconeguda'}</p>
      <p className={estils.preu}>{preuFormatat} € / hora</p>

      {/* Avís: es mostra o no es mostra. Cas de manual per a && */}
      {enTaller && (
        <p className={estils.avisTaller}>En revisió: no admet reserves de moment.</p>
      )}

      {/* Acció: dues alternatives concretes. Cas de manual per al ternari */}
      {disponible ? (
        <button type="button" className={estils.accio} onClick={gestionarClicReservar}>
          Reservar
        </button>
      ) : (
        <p className={estils.senseAccio}>Torna a consultar-la més tard.</p>
      )}
    </article>
  );
}

export default TargetaBicicleta;

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

Bicicleta Estat Etiqueta Avís de taller Acció
bici-001 Urbana Clàssica disponible Disponible No Botó «Reservar»
bici-002 Elèctrica Pro alquilada Llogada No «Torna a consultar-la més tard.»
bici-003 Càrrega Max mantenimiento Al taller «Torna a consultar-la més tard.»
bici-004 Urbana Clàssica disponible Disponible No Botó «Reservar»
bici-005 Elèctrica Pro disponible Disponible No Botó «Reservar»

L'estat buit del catàleg

Un catàleg que no troba res i es queda en blanc és un error de disseny clàssic: la persona usuària no sap si l'aplicació està carregant, si s'ha trencat o si de debò no hi ha resultats. La solució és un estat buit explícit.

// src/components/LlistaBicicletes.jsx
import TargetaBicicleta from './TargetaBicicleta.jsx';
import estils from './LlistaBicicletes.module.css';

/**
 * Secció del catàleg de CicloUrbano.
 * Props:
 *  - primera, segona, tercera (objectes bicicleta, opcionals)
 *  - alSeleccionar, alReservar (funcions, opcionals)
 *
 * (A 03-03 aquestes tres props desapareixen i passa a rebre l'array complet.)
 */
function LlistaBicicletes({ primera, segona, tercera, alSeleccionar, alReservar }) {
  // Comptem quantes props porten realment una bicicleta
  const rebudes = [primera, segona, tercera].filter(Boolean);
  const hiHaBicicletes = rebudes.length > 0;

  return (
    <section className={estils.llista}>
      <h2>Bicicletes del catàleg</h2>

      {hiHaBicicletes ? (
        <>
          <p className={estils.recompte}>
            {rebudes.length === 1
              ? "S'ha trobat 1 bicicleta."
              : `S'han trobat ${rebudes.length} bicicletes.`}
          </p>

          {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}
            />
          )}
        </>
      ) : (
        <p className={estils.buit}>
          No hi ha bicicletes que coincideixin amb el filtre. Prova amb un altre tipus.
        </p>
      )}
    </section>
  );
}

export default LlistaBicicletes;

Sí, escriure tres vegades la mateixa targeta amb un && cadascuna és feixuc, i aquesta lletjor continua sent deliberada: és l'últim avís abans de la refactorització. A la propera lliçó, map elimina les tres props i les tres repeticions d'una plomada.

El flux de decisió complet de la targeta, resumit:

flowchart TD
    A["TargetaBicicleta rep bicicleta"] --> B{"estat === 'disponible'?"}
    B -- Sí --> C["Etiqueta 'Disponible'<br/>Botó «Reservar»"]
    B -- No --> D{"estat === 'mantenimiento'?"}
    D -- Sí --> E["Etiqueta 'Al taller'<br/>Avís de revisió<br/>Sense botó"]
    D -- No --> F["Etiqueta 'Llogada'<br/>Missatge 'torna més tard'<br/>Sense botó"]

Errors Comuns i Consells

  • Escriure if dins del JSX. {if (x) ...} és un error de sintaxi: dins de les claus només caben expressions. Fes servir ternari, &&, o treu l'if abans del return.
  • El 0 fantasma amb &&. {llista.length && <p>…</p>} pinta un 0 quan la llista és buida. Escriu sempre una comparació: llista.length > 0 && ….
  • Fer servir || amb números. {places || 'sense dades'} tracta el 0 com a absència. Fes servir ?? llevat que vulguis justament el contrari.
  • Ternaris imbricats de tres o més nivells. Es tornen impossibles de llegir i de modificar. Passa a un objecte de correspondències o a components amb nom.
  • Oblidar el default en un switch. Un valor inesperat retorna undefined i no es pinta res, sense cap error visible.
  • Anomenar en minúscula un component tret d'un objecte. const avis = AVISOS[estat] i després <avis /> crea una etiqueta HTML inexistent. Ha de ser const Avis = … i <Avis />.
  • Creure que return null desmunta el component. No ho fa: el component continua a l'arbre amb el seu estat. Per desmuntar-lo de debò, el pare ha de deixar de renderitzar-lo.
  • Amagar amb CSS el que hauria de no renderitzar-se. style={{ display: 'none' }} deixa el marcatge al DOM, amb el seu cost i accessible per als lectors de pantalla. Si no ha de ser-hi, no ho pintis.
  • Definir l'objecte de correspondències dins del component. Es recrea a cada render sense motiu. Declara'l com a constant fora.
  • Consell: pensa sempre en el cas buit i en el cas d'error. «Sense resultats», «carregant», «falten dades». Les interfícies es trenquen als extrems, no al cas feliç.
  • Consell: extreu la condició a una variable amb nom. const disponible = bicicleta.estat === 'disponible' es llegeix molt millor dins del JSX que la comparació completa repetida tres vegades.
  • Consell: si el marcatge de dues branques és gairebé idèntic, no dupliquis el JSX. Condiciona només el que canvia (una classe, un text, un atribut).

Exercicis

Exercici 1

Aquest component té quatre problemes de renderitzat condicional. Localitza'ls, explica el símptoma de cadascun i reescriu-lo amb les tècniques adequades.

function ResumEstacio({ estacio, bicicletesEnEstacio, notaOperari }) {
  return (
    <div className="resum-estacio">
      <h3>{estacio.nom}</h3>
      <p>Barri: {estacio.barri || 'Sense assignar'}</p>
      {bicicletesEnEstacio.length && <p>{bicicletesEnEstacio.length} bicicletes aparcades</p>}
      <p>Places lliures: {estacio.places - bicicletesEnEstacio.length || 'cap'}</p>
      {notaOperari ? <p className="nota">{notaOperari}</p> : null}
    </div>
  );
}

Exercici 2

Crea el component PanellReserva (src/components/PanellReserva.jsx) partint del que vas escriure a 02-04. Ha de:

  • Rebre bicicleta (que pot ser undefined) i hores amb valor per defecte 1.
  • Si no hi ha bicicleta, mostrar «Selecciona una bicicleta del catàleg.» mitjançant un retorn anticipat.
  • Si la bicicleta no està disponible, mostrar un avís amb el motiu, diferent segons sigui alquilada o mantenimiento, fent servir un objecte de correspondències.
  • Si està disponible, mostrar el model, les hores, el preu total calculat i un botó «Confirmar reserva».
  • Mostrar a més, només quan el total superi els 10 €, un avís de «Reserva de llarga durada: cal confirmació per correu.»

Exercici 3

Per a cada situació de CicloUrbano, indica quina tècnica de les sis faries servir i escriu la línia o el bloc corresponent. Justifica la tria en una frase.

  1. Mostrar una insígnia «Elèctrica» només si bicicleta.tipus === 'electrica'.
  2. Pintar la icona d'una estació segons el seu barri: Centre, Nord o Eixample, amb una icona diferent per a cadascun.
  3. Mostrar «Carregant…» mentre bicicletas sigui undefined i el catàleg quan ja hagi arribat.
  4. Mostrar l'email de l'usuari o «Sense correu registrat» si val null.
  5. Triar entre un botó «Reservar» i un «Cancel·lar reserva» segons si l'usuari ja té una reserva activa.

Solucions

Solució 1.

Els quatre problemes:

Línia Problema Símptoma
estacio.barri || 'Sense assignar' Hauria de ser ?? Si el barri fos una cadena buida legítima, se substituiria; amb ?? només se substitueix null/undefined
bicicletesEnEstacio.length && … El 0 fantasma Amb l'estació buida apareix un 0 solt en pantalla
estacio.places - … || 'cap' 0 tractat com a falsy Amb l'estació plena, 0 places lliures es converteix en 'cap'; pot ser acceptable, però és accidental, no intencionat, i a més amaga el número real
notaOperari ? … : null Ternari amb null innecessari Funciona, però && és més directe. I si notaOperari pogués ser un número, el 0 tornaria a colar-se
// src/components/ResumEstacio.jsx

/**
 * Resum d'ocupació d'una estació de CicloUrbano.
 * Props:
 *  - estacio (objecte, obligatori) { id, nom, barri, places }
 *  - bicicletesEnEstacio (array, opcional, per defecte [])
 *  - notaOperari (cadena, opcional)
 */
function ResumEstacio({ estacio, bicicletesEnEstacio = [], notaOperari }) {
  const aparcades = bicicletesEnEstacio.length;
  const lliures = estacio.places - aparcades;

  return (
    <div className="resum-estacio">
      <h3>{estacio.nom}</h3>
      <p>Barri: {estacio.barri ?? 'Sense assignar'}</p>

      {aparcades > 0 ? (
        <p>{aparcades === 1 ? '1 bicicleta aparcada' : `${aparcades} bicicletes aparcades`}</p>
      ) : (
        <p>No hi ha bicicletes aparcades.</p>
      )}

      <p>Places lliures: {lliures}</p>
      {lliures === 0 && <p className="avis">Estació completa.</p>}

      {typeof notaOperari === 'string' && notaOperari.length > 0 && (
        <p className="nota">{notaOperari}</p>
      )}
    </div>
  );
}

export default ResumEstacio;

Extreure aparcades i lliures a variables amb nom elimina de pas el càlcul repetit i fa les condicions llegibles.

Solució 2.

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

const MOTIUS_NO_DISPONIBLE = {
  alquilada: 'Aquesta bicicleta està llogada ara mateix.',
  mantenimiento: 'Aquesta bicicleta és al taller i no admet reserves.'
};

/**
 * Panell de confirmació d'una reserva de CicloUrbano.
 * Props:
 *  - bicicleta (objecte, opcional): si falta, el panell convida a triar-ne una
 *  - hores (número, opcional, per defecte 1)
 *  - alConfirmar (funció, opcional): rep { bicicleta, hores, total }
 */
function PanellReserva({ bicicleta, hores = 1, alConfirmar }) {
  // Clàusula de guarda: sense bicicleta no hi ha res a calcular
  if (!bicicleta) {
    return <p className={estils.buit}>Selecciona una bicicleta del catàleg.</p>;
  }

  const disponible = bicicleta.estat === 'disponible';

  if (!disponible) {
    const motiu = MOTIUS_NO_DISPONIBLE[bicicleta.estat] ?? 'Aquesta bicicleta no està disponible.';
    return (
      <div className={estils.panell}>
        <h3>{bicicleta.model}</h3>
        <p className={estils.avis}>{motiu}</p>
      </div>
    );
  }

  const total = bicicleta.preuHora * hores;
  const totalFormatat = total.toFixed(2).replace('.', ',');

  return (
    <div className={estils.panell}>
      <h3>{bicicleta.model}</h3>
      <p>Durada: {hores === 1 ? '1 hora' : `${hores} hores`}</p>
      <p className={estils.total}>Total: {totalFormatat} €</p>

      {total > 10 && (
        <p className={estils.avisLlarg}>
          Reserva de llarga durada: cal confirmació per correu.
        </p>
      )}

      <button
        type="button"
        onClick={() => alConfirmar && alConfirmar({ bicicleta, hores, total })}
      >
        Confirmar reserva
      </button>
    </div>
  );
}

export default PanellReserva;

Els dos retorns anticipats deixen el cos principal lliure d'imbricament: quan el codi arriba a l'última part, ja sabem amb certesa que hi ha bicicleta i que està disponible. I total, totalFormatat i disponible són valors derivats, no estat.

Solució 3.

Cas Tècnica Codi
1. Insígnia «Elèctrica» && — és mostrar o res {bicicleta.tipus === 'electrica' && <span className="insignia">Elèctrica</span>}
2. Icona per barri Objecte de correspondències — tres casos enumerables const ICONES = { Centre: '🏛️', Nord: '🌳', Eixample: '🚉' }; i després {ICONES[estacio.barri] ?? '📍'}
3. «Carregant…» Retorn anticipat — tot o res if (!bicicletas) { return <p>Carregant…</p>; } abans del return principal
4. Email o alternativa ?? — la dada pot ser null <p>{usuari.email ?? 'Sense correu registrat'}</p>
5. Reservar o cancel·lar Ternari — exactament dues alternatives {teReservaActiva ? <BotoCancelar /> : <BotoReservar />}

En el cas 2, el ?? '📍' protegeix davant d'un barri nou que encara no tingui icona assignada: la interfície mostra un marcador genèric en lloc d'un buit.

Conclusió

El renderitzat condicional a React no afegeix sintaxi nova: aprofita el JavaScript que ja coneixes i el combina amb una propietat clau del JSX, que null, undefined, false i true no produeixen marcatge. Sobre aquesta base has vist sis tècniques i, sobretot, quan fer servir cadascuna: l'if/else abans del return per a branques llargues; el ternari per triar entre dues alternatives concretes, amb l'imbricament com a límit clar; el && per a mostrar-o-res, que és el cas més freqüent; el ?? per a dades que poden faltar, sense el problema que || té amb el 0; l'objecte de correspondències per a conjunts enumerables d'estats, pla i ampliable amb una línia; i el switch en funció auxiliar quan cada branca necessita lògica pròpia. A això s'afegeixen dues eines d'estructura: el retorn anticipat amb return null o amb una clàusula de guarda, i l'extracció de branques a components amb nom quan el condicional comença a comptar parèntesis.

Has tancat a més la trampa del && que va quedar oberta a la lliçó de JSX, i ara la coneixes en la seva forma més perillosa: {llista.length && …} pinta un 0 en pantalla quan la llista és buida. La regla és innegociable: a l'esquerra de &&, sempre una comparació.

CicloUrbano ho nota: EtiquetaEstat tradueix l'estat tècnic a un text llegible mitjançant un mapa ampliable, TargetaBicicleta amaga el botó «Reservar» de les bicicletes que no l'admeten i avisa de les que són al taller, i el catàleg ja té un estat buit que explica què ha passat en lloc de deixar un buit en blanc.

Però la llista continua escrivint-se targeta a targeta, amb tres props numerades i tres blocs && gairebé idèntics. És l'última peça lletja que queda del Mòdul 2, i també el deute que arrosseguem des de la lliçó de reconciliació, on es va prometre explicar per a què serveixen realment les key. Totes dues es salden alhora a Llistes i Claus, on una sola línia amb map substituirà tota aquesta bastida.

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