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
- Què és un gestor d'esdeveniments i on viu
onClickdavant d'onclick: les diferències reals- Passar la funció, no cridar-la
- L'objecte d'esdeveniment sintètic
preventDefaultistopPropagation- Gestors amb arguments: funcions fletxa en línia
- Propagació i captura
- Catàleg d'esdeveniments habituals
- On definir el gestor i com anomenar-lo
- CicloUrbano: un selector que respon i una targeta que avisa
- 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, comclassNameotabIndex, 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.
onClick davant d'onclick: les diferències reals
onClick davant d'onclick: les diferències realsSi 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 | Sí, 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)"]
- 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ó:
- El missatge del
console.logapareix just en carregar la pàgina, sense que ningú hagi premut res. - En prémer el botó no passa absolutament res, perquè
onClickésundefined. - 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.
- 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 trobesesdeveniment.persist()en un tutorial, és codi antic: a React 19 no cal i no fa res.
preventDefault i stopPropagation
preventDefault i stopPropagationSó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.
- 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:
El que passa aquí, pas a pas:
() => seleccionar('urbana')crea una funció nova que encara no s'executa.- Aquesta funció és el que s'assigna a
onClick. - 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:
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.
- Propagació i captura
Quan prems un element imbricat, l'esdeveniment no afecta només aquest element: recorre l'arbre en dues fases.
- Fase de captura: l'esdeveniment baixa des de l'arrel fins a l'element premut.
- 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 Capture —onClickCapture, 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.
- 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:
onChangede React no és elchangedel 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.onMouseEnterionMouseLeaveno bombollegen, a diferència d'onMouseOverionMouseOut. Per això són els recomanats per a efectes visuals: no es disparen en passar per sobre dels fills.
- 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.
- 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 eltipusde 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>valtype="submit"per defecte i enviaria el formulari en prémer-lo. És una precaució barata que evita un error desconcertant.onKeyDownal contenidor: gràcies al bombolleig, un sol gestor cobreix els quatre botons. En prémerEscapeamb 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:
alSeleccionarialReservarsó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
onClicken 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.
onclickno és una prop de React: React el tracta com un atribut desconegut i avisa per consola amb un missatge que suggereixonClick. 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 falsecancel·li el comportament per defecte. A React el valor de retorn s'ignora: fes serviresdeveniment.preventDefault(). - Confondre
preventDefaultambstopPropagation. 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
targetambcurrentTarget. Si l'element prémable conté una icona o un<span>,targetserà aquest fill. Per identificar l'element del gestor, fes servircurrentTarget. - Oblidar
type="button"en botons dins d'un formulari. El valor per defecte éssubmit, 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
onClicken 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,tabIndexi gestor de teclat, com veuràs a 03-06. - Consell: un gestor, una responsabilitat. Si
gestionarClicguarda l'estat, avisa el pare i registra analítica, extreu funcions auxiliars. - Consell: aprofita el bombolleig. Un sol
onKeyDownal 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
bicicletai dues props de funció:alSeleccionarialMarcarManteniment. - 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.tagNamedes de quin element s'ha executat cada gestor, per comprovar la diferència ambtarget.
Exercici 3
Afegeix a SelectorTipus un comportament de teclat més complet, fent servir un únic gestor al contenidor:
Escapetorna al tipus'todos'(ja està fet).- La tecla
ArrowRightavança al tipus següent de la constantTIPUS, iArrowLeftretrocedeix, 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
- Què és React?
- Configuració de l'Entorn de Desenvolupament
- Hola Món amb React
- JSX: Extensió de Sintaxi de JavaScript
- Com Renderitza React: Virtual DOM i Reconciliació
Mòdul 2: Components de React
- Entendre els Components
- Components Funcionals vs de Classe
- Props: Passar Dades als Components
- State: Gestió de l'Estat del Component
- Estils en els Components: CSS, Mòduls i Utilitats
Mòdul 3: Treballar amb Esdeveniments
- Gestió d'Esdeveniments a React
- Renderitzat Condicional
- Llistes i Claus
- Formularis i Components Controlats
- Validació de Formularis i Components No Controlats
- Accessibilitat en Components Interactius
Mòdul 4: Conceptes Avançats de Components
- Elevar l'Estat
- Composició vs Herència
- Mètodes del Cicle de Vida de React
- Hooks: Introducció i Ús Bàsic
- Límits d'Error: Capturar Fallades a la Interfície
Mòdul 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef i Accés al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalitzats
Mòdul 6: Enrutament a React
- Introducció a React Router
- Configuració de React Router
- Rutes Imbricades
- Navegació Programàtica
- Rutes Protegides i Control d'Accés
Mòdul 7: Gestió de l'Estat
- Introducció a la Gestió de l'Estat
- API de Context
- Redux: Introducció i Configuració
- Redux: Accions i Reductors
- Redux: Connectar-lo a React
- Estat del Servidor: Peticions, Memòria Cau i Sincronització
Mòdul 8: Optimització del Rendiment
- Tècniques d'Optimització del Rendiment a React
- Memoïtzació amb React.memo
- Hooks useMemo i useCallback
- Divisió de Codi i Càrrega Mandrosa
- Mesurar el Rendiment amb React DevTools Profiler
Mòdul 9: Proves a React
- Introducció a les Proves
- Proves Unitàries amb Jest
- Proves de Components amb React Testing Library
- Proves de Codi Asíncron i Simulació d'APIs
- Proves d'Extrem a Extrem amb Cypress
Mòdul 10: Temes Avançats
- Renderitzat al Servidor (SSR) amb Next.js
- Generació de Llocs Estàtics (SSG) amb Next.js
- Suspense i React Server Components
- TypeScript amb React
- React Native: Creació d'Aplicacions Mòbils
