En tancar el mòdul anterior vam deixar-ho dit: el catàleg de CicloUrbano és un aparador. Les dades arriben, les targetes es pinten amb el seu color i el seu distintiu d'estat, però l'aplicació no reacciona a res del que fa la persona que la fa servir. Falta el cable que uneix la interfície amb la lògica, i aquest cable són els esdeveniments. En aquesta lliçó aprendràs com React connecta un clic, una pulsació de tecla o un canvi de focus amb una funció teva: la sintaxi exacta, l'error clàssic de cridar el gestor en lloc de passar-lo, què és un esdeveniment sintètic i quina API t'ofereix, com passar arguments, com funcionen la propagació i la captura, i com anomenar els gestors perquè el codi continuï sent llegible d'aquí a sis mesos. Al final, SelectorTipus deixarà de ser una maqueta i TargetaBicicleta avisarà el seu pare de quina bicicleta s'ha premut.

Contingut

  1. Què és un gestor d'esdeveniments i on viu
  2. onClick davant d'onclick: les diferències reals
  3. Passar la funció, no cridar-la
  4. L'objecte d'esdeveniment sintètic
  5. preventDefault i stopPropagation
  6. Gestors amb arguments: funcions fletxa en línia
  7. Propagació i captura
  8. Catàleg d'esdeveniments habituals
  9. On definir el gestor i com anomenar-lo
  10. CicloUrbano: un selector que respon i una targeta que avisa

  1. Què és un gestor d'esdeveniments i on viu

Un gestor d'esdeveniments (event handler) és una funció normal i corrent que React executa quan passa alguna cosa a la interfície: un clic, una tecla, un enviament de formulari. No té res d'especial: és una funció de JavaScript declarada dins del cos del teu component.

// src/components/BotoProva.jsx
function BotoProva() {
  // 1. Es declara una funció dins del component
  function gestionarClic() {
    console.log('Han premut el botó');
  }

  // 2. S'entrega a React a través d'una prop de l'element
  return <button onClick={gestionarClic}>Prem aquí</button>;
}

export default BotoProva;

Tres detalls que convé fixar des del primer minut:

  • La funció es declara dins del component, no fora. Així pot llegir les props i l'estat d'aquesta instància concreta, que és justament el que necessitarà tan bon punt faci alguna cosa útil.
  • onClick és una prop, no un atribut d'HTML. S'escriu en camelCase, com className o tabIndex, perquè JSX fa servir noms de propietat, no d'atribut.
  • El valor va entre claus, perquè és una expressió JavaScript: la referència a la funció. Entre cometes seria una cadena de text, i React avisaria amb un error.

Aquest patró —declarar la funció a dalt, referenciar-la a baix— és el que veuràs en el 95 % del codi React que et trobis.

  1. onClick davant d'onclick: les diferències reals

Si véns de JavaScript sense frameworks coneixes dues formes d'escoltar un clic: l'atribut onclick="..." a l'HTML i element.addEventListener('click', fn) a l'script. React substitueix totes dues per una tercera.

Aspecte HTML onclick="..." DOM addEventListener React onClick={...}
Escriptura Tot en minúscula Nom en minúscula com a cadena camelCase: onClick, onMouseEnter
Valor Una cadena amb codi Una referència a funció Una referència a funció
On s'escriu Al marcatge Al JavaScript, buscant el node Al marcatge, però és JavaScript
Quants escoltadors Un; el segon trepitja el primer Tots els que vulguis Un per prop; per a diversos, una funció que en cridi diverses
Cal retirar-lo No , amb removeEventListener, o hi ha fuga de memòria No: React ho fa en desmuntar
Àmbit de les variables Global (window) El del tancament on es defineix El del component: props i estat a mà
Cal buscar el node No Sí (getElementById, querySelector) No: el gestor viatja amb l'element
Objecte d'esdeveniment Natiu Natiu Sintètic (embolcall, vegeu l'apartat 4)

Fixa't en les dues files més importants. La primera és que a React el valor és una funció, no una cadena de codi: no hi ha avaluació de text, l'editor autocompleta i els errors de sintaxi es detecten en compilar. La segona és que no has de retirar l'escoltador: quan el component desapareix de la pantalla, React neteja per tu. Amb addEventListener manual, oblidar-se el removeEventListener és una de les fonts de fugues de memòria més habituals.

Un matís tècnic que convé saber, encara que no canvia com escrius el codi: React no col·loca un escoltador a cada <button>. Adjunta uns pocs escoltadors al node arrel de l'aplicació (el <div id="root"> que vas veure a la lliçó 01-03) i, quan arriba un esdeveniment, decideix quin gestor teu ha d'executar-se. Aquesta tècnica s'anomena delegació d'esdeveniments i és la que permet que tenir mil botons en pantalla no costi mil escoltadors.

flowchart LR
    A["Clic al navegador"] --> B["Escoltador únic<br/>a l'arrel de React"]
    B --> C["React identifica<br/>l'element de l'arbre"]
    C --> D["Executa la teva funció<br/>gestionarClic(esdeveniment)"]

  1. Passar la funció, no cridar-la

Aquest és l'error més repetit de qui comença amb esdeveniments a React, i mereix el seu propi apartat perquè el símptoma desorienta molt.

// CORRECTE: passem la referència a la funció. React la cridarà quan toqui
<button onClick={gestionarClic}>Reservar</button>

// INCORRECTE: la cridem nosaltres durant el render
<button onClick={gestionarClic()}>Reservar</button>

Què passa exactament a la segona línia? Recorda que el JSX s'avalua mentre es renderitza. Els parèntesis executen la funció en aquest precís moment, i el que s'assigna a onClick no és la funció, sinó el seu valor de retorn. Com que gestionarClic no retorna res, onClick acaba valent undefined.

Conseqüències, per ordre d'aparició:

  1. El missatge del console.log apareix just en carregar la pàgina, sense que ningú hagi premut res.
  2. En prémer el botó no passa absolutament res, perquè onClick és undefined.
  3. Si el gestor actualitza l'estat, es dispara un nou render, que torna a cridar la funció, que torna a actualitzar l'estat… bucle infinit i la pestanya es queda penjada amb un error de «Too many re-renders».

La regla mnemotècnica és senzilla: amb parèntesis, ara; sense parèntesis, després.

onClick={gestionarClic}      // després (quan es premi)  ✔
onClick={gestionarClic()}    // ara (durant el render)    ✘
onClick={() => gestionarClic()}  // després, embolcallat en una altra funció  ✔

La tercera forma és vàlida i la necessitaràs a l'apartat 6, quan calgui passar arguments. Però si el gestor no porta arguments, escriu la primera: és més curta i més directa.

  1. L'objecte d'esdeveniment sintètic

Quan React executa el teu gestor, li passa un argument: l'objecte de l'esdeveniment.

function gestionarClic(esdeveniment) {
  console.log(esdeveniment.type);            // "click"
  console.log(esdeveniment.target);          // el node del DOM que ha rebut el clic
  console.log(esdeveniment.currentTarget);   // el node on hi ha el gestor
}

No és l'esdeveniment natiu del navegador: és un esdeveniment sintètic (SyntheticEvent), un objecte que React construeix embolcallant el natiu.

Per què existeix

En els anys en què React va néixer, cada navegador exposava els esdeveniments d'una forma lleugerament diferent: noms de propietat diferents, comportaments incompatibles, preventDefault que en algun navegador es deia d'una altra manera. L'esdeveniment sintètic resol tres coses:

Motiu Què aporta
Normalització La mateixa API a tots els navegadors; el teu codi no porta condicionals per navegador
Compatibilitat amb la delegació React pot reconstruir el recorregut de l'esdeveniment a través de l'arbre de components, que no sempre coincideix amb l'arbre del DOM
Interfície estable React garanteix quines propietats existeixen, encara que el navegador canviï

L'API sintètica imita la nativa, així que tot el que saps d'esdeveniments del DOM continua sent vàlid. Les propietats i mètodes que faràs servir cada dia:

Membre Què és
esdeveniment.target L'element on s'ha originat l'esdeveniment (el més profund)
esdeveniment.currentTarget L'element al qual està adjunt el gestor que s'està executant
esdeveniment.type El tipus d'esdeveniment com a cadena: "click", "submit", "keydown"
esdeveniment.preventDefault() Cancel·la l'acció per defecte del navegador
esdeveniment.stopPropagation() Impedeix que l'esdeveniment continuï pujant per l'arbre
esdeveniment.key En esdeveniments de teclat, la tecla premuda: "Enter", "Escape", "a"
esdeveniment.nativeEvent L'esdeveniment natiu original, per si necessites alguna cosa que el sintètic no exposa

La distinció entre target i currentTarget importa. Si un <button> conté un <span> i prems sobre el text, esdeveniment.target serà el <span>, mentre que esdeveniment.currentTarget serà el <button>. Quan vulguis identificar «l'element on he posat el gestor», fes servir sempre currentTarget.

function BotoAmbIcona() {
  function gestionarClic(esdeveniment) {
    console.log(esdeveniment.target.tagName);        // "SPAN" si prems el text
    console.log(esdeveniment.currentTarget.tagName); // "BUTTON" sempre
  }

  return (
    <button onClick={gestionarClic}>
      <span>Reservar</span>
    </button>
  );
}

Nota històrica útil: fins a React 16, els esdeveniments sintètics es reutilitzaven (pooling): en acabar el gestor, les seves propietats es buidaven, i per llegir-les més tard calia cridar esdeveniment.persist(). Des de React 17 això va desaparèixer. Si trobes esdeveniment.persist() en un tutorial, és codi antic: a React 19 no cal i no fa res.

  1. preventDefault i stopPropagation

Són dos mètodes diferents que es confonen constantment. Fan coses que no tenen res a veure.

preventDefault(): cancel·lar el que faria el navegador

Alguns elements tenen un comportament natiu associat: un <a href> navega, un <form> s'envia i recarrega la pàgina, un <input type="checkbox"> es marca. preventDefault() cancel·la aquest comportament.

function EnllacEstacio() {
  function gestionarClic(esdeveniment) {
    esdeveniment.preventDefault();   // el navegador NO navega a /estaciones
    console.log('Mostrar el detall sense canviar de pàgina');
  }

  return (
    <a href="/estaciones" onClick={gestionarClic}>
      Veure estacions
    </a>
  );
}

A React hi ha un detall important: retornar false des del gestor no cancel·la res. En HTML clàssic, onclick="return false" sí que ho feia. A React el valor de retorn s'ignora per complet: cal cridar explícitament esdeveniment.preventDefault().

El seu ús més freqüent, de llarg, és l'enviament de formularis, que veuràs a fons a Formularis i Components Controlats.

stopPropagation(): tallar l'ascens de l'esdeveniment

Impedeix que l'esdeveniment continuï pujant cap als elements avantpassats. És l'eina per al cas «tinc un botó dins d'una targeta prémable i no vull que prémer el botó activi també la targeta».

function TargetaAmbBoto() {
  function gestionarClicTargeta() {
    console.log('S\'ha seleccionat la targeta');
  }

  function gestionarClicBoto(esdeveniment) {
    esdeveniment.stopPropagation();   // sense això, també s'executaria gestionarClicTargeta
    console.log('S\'ha premut el botó de reservar');
  }

  return (
    <article onClick={gestionarClicTargeta}>
      <h3>Urbana Clàssica</h3>
      <button onClick={gestionarClicBoto}>Reservar</button>
    </article>
  );
}
Mètode Què cancel·la Exemple típic
preventDefault() L'acció per defecte del navegador Que un <form> recarregui la pàgina
stopPropagation() La pujada de l'esdeveniment cap als avantpassats Un botó dins d'una targeta prémable

Tots dos són independents: pots cridar l'un, l'altre, tots dos o cap.

  1. Gestors amb arguments: funcions fletxa en línia

Molt sovint el gestor necessita saber sobre què s'ha actuat: quin tipus de bicicleta, quin identificador, quina fila. I aquí xoca amb la regla de l'apartat 3: no podem escriure onClick={seleccionar('urbana')} perquè això l'executaria durant el render.

La solució idiomàtica és embolcallar-ho en una funció fletxa en línia:

<button onClick={() => seleccionar('urbana')}>Urbanes</button>

El que passa aquí, pas a pas:

  1. () => seleccionar('urbana') crea una funció nova que encara no s'executa.
  2. Aquesta funció és el que s'assigna a onClick.
  3. Quan algú prem, React crida aquesta funció, i llavors s'executa seleccionar('urbana').

Si a més necessites l'objecte d'esdeveniment, declara'l com a paràmetre de la fletxa i passa'l:

<button onClick={(esdeveniment) => seleccionar('urbana', esdeveniment)}>Urbanes</button>

L'alternativa: una funció que retorna una funció

Existeix un patró equivalent, una mica més sofisticat, anomenat currificació:

function LlistaTipus() {
  // Rep el tipus i RETORNA el gestor ja preparat
  const alSeleccionarTipus = (tipus) => (esdeveniment) => {
    console.log('Tipus triat:', tipus, 'des de', esdeveniment.currentTarget.textContent);
  };

  return (
    <>
      <button onClick={alSeleccionarTipus('urbana')}>Urbanes</button>
      <button onClick={alSeleccionarTipus('electrica')}>Elèctriques</button>
    </>
  );
}

Aquí alSeleccionarTipus('urbana') sí que s'executa durant el render, però no passa res: el que retorna és una altra funció, i és aquesta la que s'assigna a onClick. És correcte, encara que per a principiants la fletxa en línia és més fàcil de llegir.

El cost de crear la funció a cada render

Cada vegada que el component es renderitza, l'expressió () => seleccionar('urbana') produeix una funció nova. És un objecte diferent del de la vegada anterior, encara que faci exactament el mateix.

És un problema? En la immensa majoria dels casos, no. Crear una funció en JavaScript és una operació baratíssima, i la llegibilitat que hi guanyes compensa de sobres. Només comença a importar quan aquesta funció es passa com a prop a un component memoïtzat, perquè llavors la nova referència trenca la memoïtzació i provoca renders innecessaris.

Aquest escenari, com mesurar-lo i com resoldre'l amb useCallback és material del Mòdul 8, concretament de Hooks useMemo i useCallback. De moment queda't amb la regla pràctica: escriu fletxes en línia sense por i optimitza només quan mesuris un problema real.

  1. Propagació i captura

Quan prems un element imbricat, l'esdeveniment no afecta només aquest element: recorre l'arbre en dues fases.

  1. Fase de captura: l'esdeveniment baixa des de l'arrel fins a l'element premut.
  2. Fase de bombolleig (bubbling): l'esdeveniment puja des de l'element premut fins a l'arrel.

React et permet escoltar en totes dues. Per defecte, onClick escolta en el bombolleig. Afegint el sufix CaptureonClickCapture, onFocusCapture, onKeyDownCapture— escoltes en la captura.

flowchart TD
    subgraph CAPTURA["Fase 1: captura (de fora cap endins)"]
        direction TB
        C1["main<br/>onClickCapture"] --> C2["article.targeta<br/>onClickCapture"] --> C3["button<br/>onClickCapture"]
    end
    C3 --> OBJ["Element premut:<br/>button"]
    OBJ --> B1
    subgraph BOMBOLLEIG["Fase 2: bombolleig (de dins cap enfora)"]
        direction TB
        B1["button<br/>onClick"] --> B2["article.targeta<br/>onClick"] --> B3["main<br/>onClick"]
    end

Vegem-ho amb un exemple que imprimeix el recorregut complet:

function DemostracioPropagacio() {
  return (
    <main
      onClickCapture={() => console.log('1. main - captura')}
      onClick={() => console.log('6. main - bombolleig')}
    >
      <article
        onClickCapture={() => console.log('2. article - captura')}
        onClick={() => console.log('5. article - bombolleig')}
      >
        <button
          onClickCapture={() => console.log('3. button - captura')}
          onClick={() => console.log('4. button - bombolleig')}
        >
          Prem
        </button>
      </article>
    </main>
  );
}

En prémer el botó, la consola imprimeix els sis missatges exactament en aquest ordre: 1, 2, 3, 4, 5, 6.

Per a què serveix cada fase

Fase Quan es fa servir
Bombolleig (onClick) El 99 % dels casos. És el comportament natural que esperes
Bombolleig al pare Un sol gestor al contenidor per a molts fills, identificant l'origen amb esdeveniment.target
Captura (onClickCapture) Interceptar abans que els fills: registre d'analítica, tancar un menú desplegable, bloquejar la interacció en una zona

Un detall important: stopPropagation() talla el recorregut des d'on es cridi. Si el crides a la fase de captura d'article, el botó ni tan sols arribarà a rebre el seu propi clic. Fes-lo servir amb compte: tallar la propagació pot trencar funcionalitats d'altres components que depenien de rebre aquest esdeveniment.

  1. Catàleg d'esdeveniments habituals

React admet pràcticament tots els esdeveniments del DOM, sempre en camelCase. Aquests són els que faràs servir constantment:

Prop Quan es dispara Notes pràctiques
onClick En prémer (ratolí, o Enter/Espai en un <button>) El més utilitzat. En un <button> real funciona també amb teclat
onChange En canviar el valor d'un camp A React es dispara a cada pulsació de tecla, no en perdre el focus com al DOM natiu
onInput A cada entrada de text A React és gairebé idèntic a onChange; fes servir onChange per convenció
onSubmit En enviar un <form> Va al formulari, no al botó. Gairebé sempre amb preventDefault()
onFocus En rebre el focus un camp Útil per ressaltar o mostrar ajuda contextual
onBlur En perdre el focus El moment clàssic per validar un camp (ho veuràs a 03-05)
onKeyDown En prémer una tecla Es llegeix amb esdeveniment.key. És el que permet reaccionar a Enter o Escape
onKeyUp En deixar anar una tecla Menys habitual; per a dreceres, prefereix onKeyDown
onMouseEnter En entrar el ratolí a l'element No bombolleja: ideal per a efectes d'un sol element
onMouseLeave En sortir el ratolí Parella natural de l'anterior
onDoubleClick En fer doble clic Compte amb l'accessibilitat: no hi ha equivalent amb teclat
onContextMenu En obrir el menú contextual Requereix preventDefault() per substituir-lo
onScroll En desplaçar un contenidor Es dispara molt; convé limitar-ne la freqüència

Dos avisos sobre aquesta taula:

  • onChange de React no és el change del DOM. El natiu només avisa quan el camp perd el focus després d'haver canviat; el de React avisa a cada tecla. Aquesta diferència és la que fa possibles els components controlats de 03-04.
  • onMouseEnter i onMouseLeave no bombollegen, a diferència d'onMouseOver i onMouseOut. Per això són els recomanats per a efectes visuals: no es disparen en passar per sobre dels fills.

  1. On definir el gestor i com anomenar-lo

Amb tres gestors el codi encara es llegeix; amb quinze, la disciplina de noms marca la diferència entre un component mantenible i un jeroglífic.

La convenció del curs

Cas Convenció Exemple
Funció definida dins del component Prefix gestionar + què passa gestionarClic, gestionarEnviament, gestionarCanviTipus
Prop de funció que un component rep Prefix al (o on en projectes en anglès) alSeleccionar, alReservar, alCanviarTipus
Gestor d'un element del DOM La prop nativa, sense inventar-se res onClick, onSubmit, onKeyDown

La idea de fons és la simetria: el fill declara què li pot passar (alSeleccionar) i el pare decideix què fer quan passa (gestionarSeleccio).

// El pare defineix la funció i la passa cap avall
function Cataleg() {
  function gestionarSeleccio(bicicleta) {
    console.log('Bicicleta seleccionada:', bicicleta.model);
  }

  return <TargetaBicicleta bicicleta={primeraBici} alSeleccionar={gestionarSeleccio} />;
}

// El fill només sap que ha d'avisar; no sap què passarà després
function TargetaBicicleta({ bicicleta, alSeleccionar }) {
  return <article onClick={() => alSeleccionar(bicicleta)}>…</article>;
}

Aquest patró s'anomena elevar el gestor i és la base de la comunicació fill → pare a React: les props baixen dades, les props de funció pugen avisos.

Quan definir la funció fora del JSX

Situació Recomanació
El gestor té una sola línia i no porta arguments Funció amb nom a dalt: onClick={gestionarClic}
El gestor necessita un argument del context Fletxa en línia: onClick={() => gestionarClic(tipus)}
El gestor té més de dues o tres línies Sempre funció amb nom a dalt: el JSX ha de llegir-se com a marcatge
El gestor no depèn del component Funció auxiliar a src/utilitats/, importada

La regla que ho resumeix tot: el JSX descriu la interfície, no la lògica. Si dins d'un onClick hi ha un if, un try o tres sentències, aquesta lògica pertany a una funció amb nom.

  1. CicloUrbano: un selector que respon i una targeta que avisa

Apliquem tot això a l'aplicació. Dos canvis concrets.

SelectorTipus amb gestors de veritat

La versió de 02-04 ja feia servir onClick, però amb la lògica escrita directament al JSX. Ara que sabem anomenar i organitzar gestors, queda així:

// src/components/SelectorTipus.jsx
import { useState } from 'react';
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.
 * Props:
 *  - alCanviarTipus (funció, opcional): rep el tipus triat quan canvia
 *
 * Continua guardant el tipus en el seu propi estat; a 04-01 aquest estat pujarà al pare.
 */
function SelectorTipus({ alCanviarTipus }) {
  const [tipusTriat, setTipusTriat] = useState('todos');

  function gestionarSeleccioTipus(tipus) {
    setTipusTriat(tipus);
    if (alCanviarTipus) {
      alCanviarTipus(tipus);
    }
  }

  function gestionarTeclaNetejar(esdeveniment) {
    if (esdeveniment.key === 'Escape') {
      gestionarSeleccioTipus('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)}
        >
          {ETIQUETES[tipus]}
        </button>
      ))}

      <p className={estils.seleccio}>
        Selecció actual: <strong>{ETIQUETES[tipusTriat]}</strong>
      </p>
    </div>
  );
}

export default SelectorTipus;

Què ha canviat i per què:

  • gestionarSeleccioTipus és una funció amb nom. Abans la lògica estava dins de l'onClick; ara fa dues coses (guardar l'estat i avisar el pare), i això ja no cap còmodament al JSX.
  • onClick={() => gestionarSeleccioTipus(tipus)}: fletxa en línia perquè cal passar el tipus de cada botó. Sense ella, la funció s'executaria durant el render, com vas veure a l'apartat 3.
  • alCanviarTipus és una prop de funció opcional. Si el pare la passa, el selector li avisa; si no, el component continua funcionant. És la preparació per a 04-01, on l'estat pujarà al pare i el filtre funcionarà de debò.
  • type="button" als botons. Sense això, un <button> dins d'un <form> val type="submit" per defecte i enviaria el formulari en prémer-lo. És una precaució barata que evita un error desconcertant.
  • onKeyDown al contenidor: gràcies al bombolleig, un sol gestor cobreix els quatre botons. En prémer Escape amb el focus en qualsevol d'ells, el filtre torna a «Totes».

TargetaBicicleta avisa de la bicicleta premuda

// 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 l'objecte bicicleta en prémer la targeta
 *  - alReservar (funció, opcional): rep l'objecte bicicleta en prémer «Reservar»
 */
function TargetaBicicleta({
  bicicleta,
  nomEstacio = 'Estació desconeguda',
  alSeleccionar,
  alReservar
}) {
  const preuFormatat = bicicleta.preuHora.toFixed(2).replace('.', ',');
  const noDisponible = bicicleta.estat !== 'disponible';

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

  function gestionarClicReservar(esdeveniment) {
    // Sense això, el clic pujaria fins a l'<article> i també seleccionaria la targeta
    esdeveniment.stopPropagation();
    if (alReservar) {
      alReservar(bicicleta);
    }
  }

  return (
    <article
      className={classes(estils.targeta, estils[bicicleta.tipus], noDisponible && 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}</p>
      <p className={estils.preu}>{preuFormatat} € / hora</p>

      <button type="button" className={estils.accio} onClick={gestionarClicReservar}>
        Reservar
      </button>
    </article>
  );
}

export default TargetaBicicleta;

Els dos punts clau:

  • alSeleccionar i alReservar són props opcionals. La comprovació if (alSeleccionar) evita l'error «alSeleccionar is not a function» quan el pare no les passa. Una alternativa igual de vàlida és donar un valor per defecte: alSeleccionar = () => {}.
  • esdeveniment.stopPropagation() al botó és imprescindible: el botó està dins de l'<article> que ja escolta clics, així que sense tallar la propagació cada «Reservar» dispararia també la selecció de la targeta. Aquest és el cas d'ús canònic de l'apartat 5.

Un apunt que retomarem a Accessibilitat en Components Interactius: posar onClick en un <article> el fa prémable amb el ratolí, però no amb el teclat. És un deute conscient que saldarem en tancar el mòdul.

App rep els avisos

// src/App.jsx
import { bicicletas } 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 PeuDePagina from './components/PeuDePagina.jsx';

function App() {
  function gestionarCanviTipus(tipus) {
    console.log('Tipus triat al selector:', tipus);
  }

  function gestionarSeleccioBicicleta(bicicleta) {
    console.log('Seleccionada:', bicicleta.id, bicicleta.model);
  }

  function gestionarReserva(bicicleta) {
    console.log('Reservar:', bicicleta.id, 'a', bicicleta.preuHora, '€/hora');
  }

  return (
    <>
      <Capcalera />
      <main>
        <ResumFlota flota={bicicletas} />
        <SelectorTipus alCanviarTipus={gestionarCanviTipus} />
        <LlistaBicicletes
          primera={bicicletas[0]}
          segona={bicicletas[1]}
          tercera={bicicletas[2]}
          alSeleccionar={gestionarSeleccioBicicleta}
          alReservar={gestionarReserva}
        />
      </main>
      <PeuDePagina />
    </>
  );
}

export default App;

LlistaBicicletes es limita a reenviar les dues props de funció a cada targeta. De moment App només fa console.log: guardar de debò la bicicleta seleccionada exigeix que l'estat visqui al pare, i això és exactament el tema d'Elevar l'Estat. Aquí el que importa és que el flux ja funciona: el clic passa a la targeta i la informació arriba a dalt.

flowchart TD
    APP["App<br/>gestionarSeleccioBicicleta"] -- "alSeleccionar (prop funció)" --> LIS["LlistaBicicletes"]
    LIS -- "alSeleccionar" --> TAR["TargetaBicicleta"]
    TAR -. "avís en prémer: alSeleccionar(bicicleta)" .-> APP

Obre la consola del navegador, prem una targeta i després el seu botó «Reservar»: veuràs un missatge diferent en cada cas, sense que es barregin. CicloUrbano ja respon.

Errors Comuns i Consells

  • Cridar el gestor en lloc de passar-lo. onClick={gestionarClic()} l'executa durant el render. Si a més actualitza l'estat, provoques un bucle infinit i l'error «Too many re-renders». Sense parèntesis, o embolcallat en una fletxa.
  • Escriure l'esdeveniment en minúscules. onclick no és una prop de React: React el tracta com un atribut desconegut i avisa per consola amb un missatge que suggereix onClick. Sempre camelCase.
  • Passar el gestor com a cadena. onClick="gestionarClic()" és sintaxi d'HTML; a JSX assigna la cadena de text i no funciona.
  • Esperar que return false cancel·li el comportament per defecte. A React el valor de retorn s'ignora: fes servir esdeveniment.preventDefault().
  • Confondre preventDefault amb stopPropagation. El primer cancel·la el que faria el navegador; el segon, la pujada de l'esdeveniment. No són intercanviables.
  • Abusar de stopPropagation(). Tallar la propagació «per si de cas» trenca funcionalitats que depenen del bombolleig, com tancar un menú en prémer fora. Fes-lo servir només quan tinguis un conflicte real.
  • Confondre target amb currentTarget. Si l'element prémable conté una icona o un <span>, target serà aquest fill. Per identificar l'element del gestor, fes servir currentTarget.
  • Oblidar type="button" en botons dins d'un formulari. El valor per defecte és submit, així que el botó enviarà el formulari i la pàgina es recarregarà.
  • Fer servir esdeveniment.persist(). És codi de React 16. Des de React 17 els esdeveniments ja no es reutilitzen i aquesta crida sobra.
  • Posar onClick en un <div> o un <article>. Funciona amb el ratolí i falla amb el teclat. Quan puguis, fes servir un <button>; si no, hauràs d'afegir rol, tabIndex i gestor de teclat, com veuràs a 03-06.
  • Consell: un gestor, una responsabilitat. Si gestionarClic guarda l'estat, avisa el pare i registra analítica, extreu funcions auxiliars.
  • Consell: aprofita el bombolleig. Un sol onKeyDown al contenidor cobreix tots els seus fills. Menys codi i menys possibilitats d'oblidar un cas.
  • Consell: comprova amb console.log(esdeveniment) quan alguna cosa no quadri. L'objecte sintètic és inspeccionable i sol revelar de seguida si el problema és l'element equivocat o l'esdeveniment equivocat.

Exercicis

Exercici 1

Analitza aquest component i identifica tres errors relacionats amb esdeveniments. Explica el símptoma de cadascun i escriu la versió corregida.

function BotoEstacio({ estacio }) {
  function gestionarClic(esdeveniment) {
    console.log('Estació:', estacio.nom);
    return false;   // perquè l'enllaç no navegui
  }

  return (
    <div>
      <a href="/estaciones" onclick={gestionarClic()}>
        {estacio.nom}
      </a>
    </div>
  );
}

Exercici 2

Crea el component FilaBicicleta (src/components/FilaBicicleta.jsx) com una versió compacta de la targeta, pensada per a una taula. Ha de:

  • Rebre bicicleta i dues props de funció: alSeleccionar i alMarcarManteniment.
  • Pintar una fila <tr> amb el model, el tipus, l'estat i el preu, més una cel·la amb un botó «Enviar a taller».
  • Avisar amb alSeleccionar(bicicleta) quan es premi qualsevol part de la fila.
  • Avisar amb alMarcarManteniment(bicicleta) quan es premi el botó, sense disparar a més la selecció de la fila.
  • Registrar en consola amb esdeveniment.currentTarget.tagName des de quin element s'ha executat cada gestor, per comprovar la diferència amb target.

Exercici 3

Afegeix a SelectorTipus un comportament de teclat més complet, fent servir un únic gestor al contenidor:

  • Escape torna al tipus 'todos' (ja està fet).
  • La tecla ArrowRight avança al tipus següent de la constant TIPUS, i ArrowLeft retrocedeix, totes dues de forma circular (de l'últim al primer i viceversa).
  • Qualsevol altra tecla no ha de fer res.

Escriu només el gestor i explica per què et convé calcular el nou índex a partir de TIPUS.indexOf(tipusTriat).

Solucions

Solució 1.

Els tres errors:

Error Símptoma
onclick en minúscula React no reconeix la prop; el gestor mai s'executa i apareix un avís a la consola suggerint onClick
gestionarClic() amb parèntesis La funció s'executa durant el render: el missatge apareix en carregar la pàgina i onClick acaba valent undefined
return false A React el valor de retorn s'ignora: l'enllaç navega igualment i la pàgina es recarrega. Cal cridar esdeveniment.preventDefault()
// src/components/BotoEstacio.jsx

/**
 * Enllaç a la fitxa d'una estació.
 * Props:
 *  - estacio (objecte, obligatori) { id, nom, barri, places }
 */
function BotoEstacio({ estacio }) {
  function gestionarClic(esdeveniment) {
    esdeveniment.preventDefault();   // cancel·la la navegació del navegador
    console.log('Estació:', estacio.nom);
  }

  return (
    <div>
      <a href="/estaciones" onClick={gestionarClic}>
        {estacio.nom}
      </a>
    </div>
  );
}

export default BotoEstacio;

Solució 2.

// src/components/FilaBicicleta.jsx
import EtiquetaEstat from './EtiquetaEstat.jsx';

/**
 * Fila compacta d'una bicicleta, per a vistes de taula.
 * Props:
 *  - bicicleta (objecte, obligatori)
 *  - alSeleccionar (funció, opcional): rep la bicicleta en prémer la fila
 *  - alMarcarManteniment (funció, opcional): rep la bicicleta en prémer el botó
 */
function FilaBicicleta({ bicicleta, alSeleccionar, alMarcarManteniment }) {
  const preuFormatat = bicicleta.preuHora.toFixed(2).replace('.', ',');

  function gestionarClicFila(esdeveniment) {
    console.log('Gestor a:', esdeveniment.currentTarget.tagName); // sempre "TR"
    console.log('Origen real:', esdeveniment.target.tagName);     // "TD", "SPAN"…
    if (alSeleccionar) {
      alSeleccionar(bicicleta);
    }
  }

  function gestionarClicTaller(esdeveniment) {
    esdeveniment.stopPropagation();   // evita que el clic pugi fins al <tr>
    console.log('Gestor a:', esdeveniment.currentTarget.tagName); // "BUTTON"
    if (alMarcarManteniment) {
      alMarcarManteniment(bicicleta);
    }
  }

  return (
    <tr onClick={gestionarClicFila}>
      <td>{bicicleta.model}</td>
      <td>{bicicleta.tipus}</td>
      <td>
        <EtiquetaEstat estat={bicicleta.estat} />
      </td>
      <td>{preuFormatat} €/h</td>
      <td>
        <button type="button" onClick={gestionarClicTaller}>
          Enviar a taller
        </button>
      </td>
    </tr>
  );
}

export default FilaBicicleta;

En prémer el text d'una cel·la, currentTarget és sempre TR (on hi ha el gestor) mentre que target és TD. En prémer el botó, el stopPropagation() impedeix que s'executi gestionarClicFila: a la consola només apareix el missatge del botó.

Solució 3.

function gestionarTecla(esdeveniment) {
  const indexActual = TIPUS.indexOf(tipusTriat);

  if (esdeveniment.key === 'Escape') {
    gestionarSeleccioTipus('todos');
    return;
  }

  if (esdeveniment.key === 'ArrowRight') {
    const seguent = (indexActual + 1) % TIPUS.length;
    gestionarSeleccioTipus(TIPUS[seguent]);
    return;
  }

  if (esdeveniment.key === 'ArrowLeft') {
    // Sumar TIPUS.length abans del mòdul evita índexs negatius
    const anterior = (indexActual - 1 + TIPUS.length) % TIPUS.length;
    gestionarSeleccioTipus(TIPUS[anterior]);
  }

  // Qualsevol altra tecla: no fem res i l'esdeveniment segueix el seu curs
}

Calcular l'índex a partir de TIPUS.indexOf(tipusTriat) en lloc de guardar un estat index a part evita tenir dues fonts de veritat que podrien desincronitzar-se: l'índex és un valor derivat de l'estat, exactament com vas aprendre a la lliçó 02-04. I l'operador mòdul % dona el comportament circular sense cap if addicional; el truc de sumar TIPUS.length abans del mòdul és necessari perquè en JavaScript -1 % 4 val -1, no 3.

Conclusió

Els esdeveniments són el punt on la interfície deixa de ser una imatge i es converteix en una aplicació. Ja saps que un gestor és una funció normal declarada dins del component i entregada a React mitjançant una prop en camelCase; que la prop rep la funció, no la seva crida, i per què uns parèntesis de més provoquen des d'un missatge prematur fins a un bucle infinit de renders; que React et lliura un esdeveniment sintètic que normalitza l'API entre navegadors, amb target, currentTarget, preventDefault() i stopPropagation() com a peces d'ús diari; que per passar arguments s'embolcalla el gestor en una fletxa en línia, el cost de la qual només importarà quan arribis al Mòdul 8; i que l'esdeveniment recorre l'arbre en dues fases, amb onClick escoltant en el bombolleig i onClickCapture en la captura.

A CicloUrbano això s'ha traduït en dues peces que ja responen: un SelectorTipus amb gestors anomenats, botons que ressalten la selecció i dreceres de teclat; i una TargetaBicicleta que accepta les props de funció alSeleccionar i alReservar per avisar cap amunt del que passa a dins, amb el stopPropagation() just on calia. Les dades baixen per props; els avisos pugen per props de funció. Aquest doble sentit és el flux de dades de React, i no canviarà en tot el curs.

Ara l'aplicació reacciona, però continua pintant sempre el mateix. Una bicicleta en manteniment no hauria d'oferir un botó «Reservar», un catàleg sense resultats hauria de dir-ho en lloc de quedar-se en blanc, i un avís només hauria d'aparèixer quan hi ha alguna cosa a avisar. Pintar unes coses o altres segons l'estat o les props és el següent pas, i té més matisos dels que sembla —inclosa una trampa amb l'operador && que ja va assomar a la lliçó de JSX—. Ho veuràs a Renderitzat Condicional.

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