Imagina que el servidor de CicloUrbano retorna una bicicleta sense el camp preuHora. TargetaBicicleta intenta fer bicicleta.preuHora.toFixed(2), JavaScript llança «Cannot read properties of undefined», i el resultat no és una targeta trencada: és que tota l'aplicació desapareix. Capcalera, catàleg, formulari de reserves, peu de pàgina: pantalla en blanc. Des de React 16, un error no capturat durant el render desmunta l'arbre complet, i aquest comportament —deliberat— fa imprescindible una peça que encara no coneixes: el límit d'error. En aquesta lliçó aprendràs què és, com s'implementa (amb l'única excepció de React 19 que encara exigeix un component de classe), on col·locar-lo, quins tipus d'error no captura i què fer en aquests casos, i com dissenyar un missatge de fallada que no espanti qui el llegeix.
Contingut
- Què passa avui quan un component peta
- Què és un límit d'error
- Els dos mètodes:
getDerivedStateFromErroricomponentDidCatch LimitErrorcomplet per a CicloUrbano- On col·locar els límits: global i granulars
- Què NO capturen els límits d'error
- Desenvolupament davant de producció
react-error-boundary, l'alternativa pràctica- Registrar l'error en un servei de monitorització
- Dissenyar un bon missatge de fallada
- Què passa avui quan un component peta
Anem a provocar-ho a propòsit. Afegeix una bicicleta defectuosa a les dades de CicloUrbano:
// src/dades/domini.js (només per a la demostració)
export const bicicletaDefectuosa = {
id: 'bici-999',
model: 'Prototip',
tipus: 'urbana',
estat: 'disponible',
estacioId: 'est-01'
// falta preuHora!
};TargetaBicicleta la rep i executa aquesta línia, que ja vas escriure a 02-05:
const preuFormatat = bicicleta.preuHora.toFixed(2).replace('.', ',');
// TypeError: Cannot read properties of undefined (reading 'toFixed')El que veus al navegador amb l'aplicació compilada per a producció:
I a la consola:
Uncaught TypeError: Cannot read properties of undefined (reading 'toFixed')
The above error occurred in the <TargetaBicicleta> component.
Consider adding an error boundary to your tree to customize error handling behavior.Per què React ho desmunta tot? La decisió es va prendre a React 16 i el raonament és aquest: si un component ha fallat a mig render, l'arbre queda en un estat inconsistent i desconegut. Deixar la interfície a mitges és pitjor que treure-la: es podrien mostrar saldos equivocats, botons que executen l'acció que no toca o formularis que envien dades corruptes. En una aplicació de pagaments, una interfície corrupta és més perillosa que cap interfície.
Però «pantalla en blanc» tampoc és una resposta acceptable per a la persona usuària. La solució que ofereix React és que tu decideixis què mostrar en el seu lloc, i aquesta és la feina del límit d'error.
- Què és un límit d'error
Un límit d'error (error boundary) és un component que captura els errors de JavaScript llançats en qualsevol punt del seu subarbre de fills, evita que l'aplicació sencera es desmunti i pinta en el seu lloc una interfície alternativa.
Les quatre propietats que defineixen el seu comportament:
- Captura cap avall, no cap als costats. Protegeix els seus descendents, mai els seus germans ni a si mateix.
- Es comporta com un
catchde JavaScript, però per a components. L'error «bombolleja» cap amunt per l'arbre fins a trobar el primer límit. - Substitueix el subarbre sencer. Quan captura, tot el que penjava d'ell es desmunta i es pinta l'alternativa. No hi ha reparació parcial.
- Si no hi ha cap límit en tota la cadena, l'error arriba a l'arrel i React desmunta l'aplicació: la pantalla en blanc de l'apartat 1.
flowchart TD
ROOT["main.jsx / createRoot"] --> LG["LimitError GLOBAL"]
LG --> APP["App"]
APP --> CAB["Capcalera ✅ segueix viva"]
APP --> LC["LimitError «Catàleg»"]
APP --> LR["LimitError «Reserves»"]
LC --> LIS["LlistaBicicletes"]
LIS --> T1["TargetaBicicleta"]
LIS --> T2["TargetaBicicleta 💥 error"]
LR --> FR["FormulariReserva ✅ segueix viu"]
T2 -. "l'error puja" .-> LC
style LC fill:#fde68a
style T2 fill:#fecaca
En aquest arbre, la fallada d'una targeta la captura el límit del catàleg: es perd la llista de bicicletes i es mostra un missatge en el seu lloc, però la capçalera, el formulari de reserves i el peu segueixen funcionant. Aquesta és tota la idea.
- Els dos mètodes:
getDerivedStateFromError i componentDidCatch
getDerivedStateFromError i componentDidCatchAquí arriba l'excepció anunciada a 04-03 i 04-04: no existeix cap hook equivalent. Un límit d'error ha de ser un component de classe, perquè els dos mètodes que ho fan possible només estan disponibles a les classes. React ho reconeix obertament a la seva documentació, i hi ha feina en curs per oferir una alternativa, però avui dia —React 19— la classe és obligatòria.
Els dos mètodes fan coses diferents i convé no confondre'ls.
static getDerivedStateFromError(error)
static getDerivedStateFromError(error) {
// Ha de retornar un objecte amb l'estat nou, o null per no canviar-lo
return { hiHaError: true };
}| Aspecte | Detall |
|---|---|
És static |
Pertany a la classe, no a la instància. No té this |
| Quan s'executa | Durant la fase de render, just després que un descendent llanci |
| Què rep | L'objecte d'error llançat |
| Què ha de retornar | Un objecte d'estat (es fusiona amb l'actual), o null |
| Per a què serveix | Només per decidir que cal pintar l'alternativa |
| Què NO ha de fer | Efectes secundaris: res de fetch, console.log ni registre |
La prohibició d'efectes secundaris no és una recomanació: aquest mètode s'executa durant el render, que ha de ser pur (04-03), i React pot cridar-lo més d'una vegada per al mateix error. Un registre escrit aquí es podria enviar duplicat.
componentDidCatch(error, infoError)
componentDidCatch(error, infoError) {
// infoError.componentStack: el camí de components fins a la fallada
registrarEnServei(error, infoError.componentStack);
}| Aspecte | Detall |
|---|---|
| És un mètode d'instància | Sí que té this: pot llegir props i estat |
| Quan s'executa | Després del render de l'alternativa, en la fase de commit |
| Què rep | L'error i un objecte amb componentStack |
| Per a què serveix | Efectes secundaris: registrar l'error, avisar un servei de monitorització |
| Què NO convé fer | Decidir què es pinta (per a això hi ha el mètode anterior) |
El componentStack és la informació més valuosa que obtindràs d'una fallada en producció:
in TargetaBicicleta (created by LlistaBicicletes)
in LlistaBicicletes (created by PanellAvancat)
in PanellAvancat (created by App)
in AppNo és la pila de crides de JavaScript: és el camí de components, que et diu exactament quina part de la interfície ha fallat.
Resum del repartiment de feina: getDerivedStateFromError pinta; componentDidCatch registra. Pots implementar només el primer, però llavors et quedaràs sense saber què falla en producció.
LimitError complet per a CicloUrbano
LimitError complet per a CicloUrbano// src/components/LimitError.jsx
import { Component } from 'react';
import estils from './LimitError.module.css';
/**
* Límit d'error de CicloUrbano. ÚNICA classe del projecte: els dos mètodes
* que necessita no tenen equivalent amb hooks a React 19.
*
* Props:
* - children (contingut protegit, obligatori)
* - alternativa (JSX o funció (error, reintentar) => JSX, opcional)
* - titol (cadena, opcional): encapçalament del missatge per defecte
* - alRegistrar (funció, opcional): rep (error, componentStack)
*/
class LimitError extends Component {
constructor(props) {
super(props);
this.state = { hiHaError: false, error: null };
this.reintentar = this.reintentar.bind(this);
}
// 1. PINTAR: s'executa durant el render. Sense efectes secundaris.
static getDerivedStateFromError(error) {
return { hiHaError: true, error };
}
// 2. REGISTRAR: s'executa després. Aquí sí que van els efectes secundaris.
componentDidCatch(error, infoError) {
console.error('[LimitError] fallada capturada:', error);
console.error('[LimitError] components:', infoError.componentStack);
if (this.props.alRegistrar) {
this.props.alRegistrar(error, infoError.componentStack);
}
}
// 3. REINTENTAR: tornar a l'estat sa i remuntar el subarbre
reintentar() {
this.setState({ hiHaError: false, error: null });
}
render() {
if (!this.state.hiHaError) {
return this.props.children; // cas normal: no destorba en absolut
}
const { alternativa, titol = 'No hem pogut mostrar aquesta secció' } = this.props;
// L'alternativa pot ser una funció, per rebre l'error i el reintent
if (typeof alternativa === 'function') {
return alternativa(this.state.error, this.reintentar);
}
if (alternativa) {
return alternativa;
}
// Alternativa per defecte
return (
<section className={estils.limit} role="alert">
<h2 className={estils.titol}>{titol}</h2>
<p className={estils.text}>
Hi ha hagut un problema tècnic i aquesta part de CicloUrbano no s'ha pogut
carregar. La resta de la pàgina continua funcionant amb normalitat.
</p>
<button type="button" className={estils.boto} onClick={this.reintentar}>
Tornar-ho a intentar
</button>
</section>
);
}
}
export default LimitError;Repàs de les decisions de disseny:
- L'estat guarda dues coses: l'indicador
hiHaErrorper decidir què pintar i l'erroren si, per poder mostrar detalls o passar-lo a l'alternativa. renderretornathis.props.childrensense tocar-lo mentre no hi ha error. Un límit d'error és invisible en el cas normal: no afegeix marcatge, no afegeix estils, no costa rendiment.alternativaadmet dues formes. Si és JSX, es pinta tal qual; si és una funció, se li passen(error, reintentar)perquè qui l'usa pugui construir un missatge a mida amb un botó propi. És la tècnica de render props de 04-02, aplicada on de veritat aporta.- El botó de reintent fa
setState({ hiHaError: false }). Això provoca un render normal,childrenes torna a muntar des de zero i, si la causa era transitòria —una resposta de xarxa malformada, per exemple—, la interfície es recupera. Si l'error és permanent, el límit el tornarà a capturar i es tornarà a mostrar el missatge: no hi ha bucle infinit, perquè el reintent el dispara una persona. role="alert"ve de 03-06: el missatge apareix sense que ningú mogui el focus, i les tecnologies d'assistència l'han d'anunciar.- El
binddel constructor és el de 02-02:reintentares passa com a gestor i perdria el seuthissense ell.
Ús
// src/main.jsx — el límit global
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import LimitError from './components/LimitError.jsx';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<LimitError titol="CicloUrbano no està disponible ara mateix">
<App />
</LimitError>
</StrictMode>
);// src/App.jsx — límits granulars
<LimitError
titol="No hem pogut carregar el catàleg"
alternativa={(error, reintentar) => (
<Avis
to="error"
titol="Catàleg no disponible"
accions={<button type="button" onClick={reintentar}>Reintentar</button>}
>
No hem pogut mostrar les bicicletes. Pots continuar consultant les teves reserves
mentre ho solucionem.
</Avis>
)}
>
<LlistaBicicletes
bicicletes={bicicletesVisibles}
estacions={estacions}
alSeleccionar={gestionarSeleccioBicicleta}
alReservar={gestionarReserva}
/>
</LimitError>Fixa't que l'alternativa reutilitza l'Avis genèric de 04-02: composició una altra vegada, sense una línia d'estil duplicada.
- On col·locar els límits: global i granulars
L'estratègia recomanada combina dos nivells.
Un límit global a l'arrel. És la xarxa d'última instància: garanteix que mai aparegui una pantalla en blanc, passi el que passi. Embolica <App /> a main.jsx i mostra un missatge d'aplicació completa, amb l'opció de recarregar.
Diversos límits granulars al voltant de les seccions independents. La pregunta que decideix on posar-ne un és sempre la mateixa:
Quina part de la interfície es pot perdre sense que l'aplicació deixi de ser útil?
A CicloUrbano hi ha tres respostes clares:
| Secció | Què protegeix | Què segueix funcionant si falla |
|---|---|---|
| Catàleg de bicicletes | LlistaBicicletes i les seves targetes |
Capcalera, reserves, estacions |
| Panell de reserves | PanellReserva i FormulariReserva |
El catàleg sencer es continua consultant |
| Mapa d'estacions | TargetaEstacio i els seus comptadors |
Catàleg i reserves |
flowchart TD
M["main.jsx"] --> LG["🛡️ LimitError GLOBAL<br/>«CicloUrbano no disponible»"]
LG --> APP["App"]
APP --> CAB["Capcalera<br/>(sense límit: és marcatge estàtic)"]
APP --> L1["🛡️ LimitError «Catàleg»"]
APP --> L2["🛡️ LimitError «Reserves»"]
APP --> L3["🛡️ LimitError «Estacions»"]
APP --> PIE["PeuDePagina"]
L1 --> CAT["SelectorTipus + LlistaBicicletes + TargetaBicicleta"]
L2 --> RSV["PanellReserva + FormulariReserva"]
L3 --> EST["TargetaEstacio + ComptadorPlaces"]
Criteris pràctics per decidir:
- Embolica el que consumeix dades externes. Tot el que depèn d'una API és candidat: les dades malformades són la causa número u dels errors en render.
- Embolica el que és de tercers. Un gràfic, un mapa, un reproductor: codi que no controles.
- Embolica cada ruta. Quan arribi React Router (Mòdul 6), un límit per ruta impedeix que una fallada en una pantalla ensorri la navegació sencera.
- No embolquis cada component. Un límit per targeta sembla més segur, però omple el codi de soroll i fa que una fallada es disfressi de «targeta trencada» en lloc de saltar a la vista. La granularitat adequada és la secció, no l'element.
- Recorda que un límit no es protegeix a si mateix. Si el JSX de la teva alternativa llança un error, la fallada puja al límit de dalt. Mantén les alternatives simples: text, un botó i poca cosa més, sense càlculs ni accés a camps que podrien faltar.
- Què NO capturen els límits d'error
Aquest apartat és el que evita falses sensacions de seguretat. Un límit d'error captura només errors llançats durant el render, en els mètodes del cicle de vida i en els constructors del seu subarbre. Tota la resta queda fora.
| Tipus d'error | El captura? | Què fer en el seu lloc |
|---|---|---|
| Render d'un descendent | Sí | És la seva raó de ser |
| Cicle de vida d'un descendent | Sí | — |
| Constructor d'un descendent | Sí | — |
Gestors d'esdeveniments (onClick, onSubmit) |
No | try/catch dins del gestor |
Codi asíncron (setTimeout, promeses, fetch) |
No | try/catch amb async/await, o .catch() |
| Renderitzat en servidor | No | Gestió d'errors pròpia del framework (Next.js, 10-01) |
| Errors del mateix límit | No | El captura el límit superior; mantén l'alternativa simple |
Gestors d'esdeveniments
La raó és senzilla: un gestor s'executa fora del render, quan React ja no està construint res. Una fallada allà no deixa l'arbre inconsistent, així que React no hi intervé i l'error arriba a la consola sense més.
// src/App.jsx (fragment)
function App() {
const [errorReserva, setErrorReserva] = useState(null);
async function gestionarConfirmacio({ bicicleta, hores, total }) {
setErrorReserva(null);
try {
await enviarReserva({ bicicletaId: bicicleta.id, hores, total });
setBicicletaSeleccionada(null);
} catch (error) {
console.error('[App] no s\'ha pogut crear la reserva', error);
setErrorReserva('No hem pogut confirmar la teva reserva. Torna-ho a intentar.');
}
}
return (
<>
{errorReserva && (
<Avis to="error" titol="Reserva no confirmada">{errorReserva}</Avis>
)}
{/* … */}
</>
);
}El patró és sempre el mateix: try/catch al gestor i un estat d'error propi que es pinta amb l'Avis de 04-02. És més feina que un límit, però també permet un missatge molt més precís, perquè saps exactament quina operació ha fallat.
Codi asíncron
// TRENCAT: el límit NO ho captura
useEffect(() => {
setTimeout(() => {
throw new Error('fallada diferida'); // surt del context de React
}, 1000);
}, []);// CORRECTE: capturar i convertir en estat
useEffect(() => {
let cancel·lat = false;
async function carregar() {
try {
const resposta = await fetch('/api/bicicletas');
if (!resposta.ok) throw new Error(`HTTP ${resposta.status}`);
const dades = await resposta.json();
if (!cancel·lat) setBicicletes(dades);
} catch (error) {
if (!cancel·lat) setErrorCarrega(error.message);
}
}
carregar();
return () => { cancel·lat = true; }; // neteja: la lliçó 04-03 en acció
}, []);Hi ha un truc per portar un error asíncron fins a un límit: guardar-lo en estat i rellançar-lo durant el render.
const [errorFatal, setErrorFatal] = useState(null);
if (errorFatal) {
throw errorFatal; // ara sí que passa durant el render: el límit el captura
}Fes-lo servir amb moderació i només per a errors que de veritat impedeixen mostrar la secció; per a la resta, un estat d'error i un Avis donen millor experiència. Al Mòdul 7 veuràs que les biblioteques d'estat del servidor integren això de sèrie.
- Desenvolupament davant de producció
Un detall que confon molta gent la primera vegada que prova un límit d'error: en desenvolupament, l'error es continua veient.
Desenvolupament (npm run dev) |
Producció (npm run build) |
|
|---|---|---|
| El límit captura l'error | Sí | Sí |
| Es pinta l'alternativa | Sí | Sí |
| Apareix la superposició d'errors de Vite | Sí, per sobre de tot | No |
| L'error s'imprimeix a la consola | Sí, sempre | Sí |
| Missatge de l'error | Complet, amb pila i components | Pot estar minificat |
La superposició vermella de Vite es mostra encara que el límit hagi funcionat perfectament, i és intencionat: durant el desenvolupament vols assabentar-te de tots els errors, no que se'ls empassi un catch silenciós. Tanca-la amb Escape i veuràs a sota la teva alternativa, ja pintada.
Per comprovar de debò el comportament que veurà el públic:
En aquesta compilació no hi ha superposició, i veuràs exactament el que veurà la persona usuària. És una prova que convé fer sempre abans de donar per bo un límit: és fàcil escriure una alternativa que al seu torn falla —per exemple, llegint error.response.data— i en desenvolupament no adonar-se'n perquè la superposició tapa el resultat.
react-error-boundary, l'alternativa pràctica
react-error-boundary, l'alternativa pràcticaEscriure la classe una vegada està bé; escriure-la a cada projecte, no tant. La biblioteca react-error-boundary és l'estàndard de facto de la comunitat i encapsula tot el de l'apartat 4, a més d'afegir la part que més costa fer bé: el reinici.
import { ErrorBoundary } from 'react-error-boundary';
function AlternativaCataleg({ error, resetErrorBoundary }) {
return (
<Avis
to="error"
titol="Catàleg no disponible"
accions={<button type="button" onClick={resetErrorBoundary}>Reintentar</button>}
>
No hem pogut mostrar les bicicletes ({error.message}).
</Avis>
);
}
// Ús
<ErrorBoundary
FallbackComponent={AlternativaCataleg}
onError={(error, info) => registrarEnServei(error, info.componentStack)}
onReset={() => recarregarBicicletes()}
resetKeys={[tipusTriat]}
>
<LlistaBicicletes bicicletes={bicicletesVisibles} estacions={estacions} />
</ErrorBoundary>Què aporta sobre la implementació pròpia:
| Prestació | Per a què serveix |
|---|---|
FallbackComponent |
Un component complet com a alternativa, amb error i resetErrorBoundary injectats |
onError |
El punt de registre, sense escriure componentDidCatch |
onReset |
S'executa en reintentar: el lloc on recarregar dades o netejar estat |
resetKeys |
Reinicia el límit automàticament quan canvia algun d'aquests valors |
useErrorBoundary |
Un hook per enviar a mà un error asíncron al límit més proper |
resetKeys resol un cas real: si el catàleg falla amb el filtre «De càrrega» i la persona canvia a «Urbanes», té sentit reintentar només per aquest canvi, sense prémer cap botó.
Recomanació pràctica: escriu la classe a mà una vegada per entendre el mecanisme —és el que acabes de fer— i usa react-error-boundary en projectes reals.
- Registrar l'error en un servei de monitorització
Un límit que només pinta un missatge bonic deixa el teu equip cec: les fallades de producció passen en navegadors que no tens al davant. Per això componentDidCatch és tan important.
// src/utilitats/monitoritzacio.js
/**
* Envia un error al servei de monitorització.
* Genèric a propòsit: substitueix la URL per la del teu proveïdor.
*/
export function registrarError(error, componentStack, context = {}) {
if (import.meta.env.DEV) {
console.error('[monitoritzacio] (desenvolupament, no s\'envia)', error);
return;
}
const informe = {
missatge: error.message,
pila: error.stack,
components: componentStack,
url: window.location.href,
navegador: navigator.userAgent,
moment: new Date().toISOString(),
versio: import.meta.env.VITE_VERSION_APP ?? 'desconeguda',
...context
};
// `keepalive` permet que l'enviament sobrevisqui al tancament d'una pestanya
fetch('/api/errores', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(informe),
keepalive: true
}).catch(() => {
// Si falla el registre, no fem res més: mai provoquis un segon error
});
}// Connectat al límit
<LimitError
titol="No hem pogut carregar el catàleg"
alRegistrar={(error, componentStack) =>
registrarError(error, componentStack, { seccio: 'cataleg', tipusTriat })
}
>
<LlistaBicicletes … />
</LimitError>Regles d'or del registre d'errors:
- No registris en desenvolupament. Ompliries el panell de soroll amb errors que estàs provocant tu.
- Afegeix context de negoci. Saber que la fallada va ser a la secció «catàleg» amb el filtre «càrrega» val més que la pila de crides minificada.
- Mai enviïs dades personals. El correu de
usr-01, un token de sessió o el contingut d'un formulari no han de sortir en un informe d'error. Envia identificadors, no dades. - El registre mai ha de llançar. El
.catch()buit és intencionat: un error dins del gestor d'errors és la pitjor classe d'error. - Puja els source maps. Sense ells, la pila de producció és il·legible. Puja'ls al servei, no al servidor públic.
En un projecte real usaràs un servei comercial (n'hi ha diversos molt coneguts) amb el seu propi SDK, però el patró és idèntic: un punt únic de registre cridat des de componentDidCatch o des de onError.
- Dissenyar un bon missatge de fallada
El límit funciona; ara falta que el missatge no arruïni l'experiència. Un missatge de fallada té quatre feines: explicar què ha passat, delimitar l'abast, oferir una sortida i no espantar.
| Evita | Prefereix |
|---|---|
| «Error: undefined is not a function» | «No hem pogut mostrar el catàleg» |
| «S'ha produït un error inesperat» (i res més) | Explicar quina part falla i què segueix funcionant |
| Culpar la persona («has fet alguna cosa malament») | Assumir el problema en primera persona del plural |
| Un carreró sense sortida | Un botó de reintent i una alternativa («consulta les teves reserves») |
| Un bolcat tècnic en pantalla | Un identificador curt d'incidència per a suport |
| Emojis d'alarma i colors estridents | To sobri, amb el color d'error del teu sistema (--color-manteniment) |
Un exemple aplicat a CicloUrbano:
function AlternativaCataleg({ error, resetErrorBoundary, idIncidencia }) {
return (
<section className={estils.fallada} role="alert">
<h2>No hem pogut mostrar el catàleg</h2>
<p>
Hi ha hagut un problema en carregar les bicicletes. La resta de CicloUrbano
funciona amb normalitat: pots consultar les teves reserves o cercar una estació.
</p>
<div className={estils.accions}>
<button type="button" onClick={resetErrorBoundary}>Tornar-ho a intentar</button>
<a href="/mis-reservas">Anar a les meves reserves</a>
</div>
{idIncidencia && (
<p className={estils.incidencia}>
Si el problema continua, indica aquesta referència a suport:{' '}
<code>{idIncidencia}</code>
</p>
)}
</section>
);
}Quatre detalls deliberats:
- El títol diu què ha fallat, en llenguatge de la persona usuària, no del programa.
- La segona frase delimita el dany. Saber que només s'ha ensorrat una secció canvia completament la percepció del problema.
- Hi ha dues sortides: reintentar i continuar fent una altra cosa. Un missatge sense sortida és un mur.
- L'identificador d'incidència connecta el que veu la persona amb el que veu el teu equip al panell de monitorització, sense mostrar detalls tècnics.
I una nota d'accessibilitat, tancant el fil de 03-06: el contenidor porta role="alert" perquè s'anunciï en aparèixer, i el missatge no depèn del color vermell per entendre's.
Errors Comuns i Consells
- Esperar que el límit capturi un
onClick. No ho fa, mai. Els gestors necessiten el seu propitry/catchi el seu propi estat d'error. - Esperar que capturi un
fetchfallit. Tampoc: el codi asíncron queda fora. Captura ambtry/catchi guarda l'error en estat, o rellança'l durant el render si de veritat és fatal. - Posar efectes secundaris a
getDerivedStateFromError. S'executa durant el render i es pot cridar diverses vegades: els registres sortirien duplicats. Aquesta feina és decomponentDidCatch. - Una alternativa que també pot fallar. Si el teu missatge llegeix
error.response.data.missatgei aquest camp no existeix, el límit llança dins de si mateix i la fallada puja al límit superior. Alternatives simples i a prova de dades absents. - Un sol límit global i res més. Compleix amb no deixar la pantalla en blanc, però qualsevol fallada s'emporta per davant l'aplicació sencera. Afegeix límits per secció.
- Un límit per component. L'extrem contrari: soroll, missatges minúsculs pertot arreu i fallades que passen desapercebudes.
- Provar només en desenvolupament. La superposició de Vite tapa el resultat. Comprova sempre amb
npm run build && npm run preview. - Consell: prova els teus límits a propòsit. Afegeix temporalment un component que llanci (
function Bomba() { throw new Error('prova'); }) dins de cada secció protegida i confirma que la resta de la interfície sobreviu. - Consell: un límit no substitueix la validació. Si saps que
preuHorapot faltar, escriubicicleta.preuHora ?? 0a la targeta. El límit és la xarxa de seguretat per al que no havies previst, no una excusa per no comprovar dades.
Exercicis
Exercici 1. Col·loca els límits d'error de CicloUrbano. Escriu el main.jsx amb el límit global i el fragment d'App.jsx amb tres límits granulars (catàleg, reserves i estacions), cadascun amb el seu títol propi i el seu registre a registrarError amb el context de la secció. Explica per què Capcalera i PeuDePagina queden fora.
Exercici 2. Classifica aquestes cinc fallades de CicloUrbano: les captura un límit d'error? Si no, indica com es gestiona cadascuna.
TargetaBicicletallegeixbicicleta.preuHora.toFixed(2)ipreuHoraésundefined.gestionarConfirmaciocrida unfetchque retorna 500.- Un
setTimeoutdins d'un efecte llança un error als dos segons. - El constructor d'un component de classe heretat llança en rebre props no vàlides.
- L'alternativa d'un límit intenta llegir
error.detalls.codiidetallesno existeix.
Exercici 3. Escriu AlternativaReserves, la interfície de reserva per al límit del panell de reserves. Requisits: reutilitzar el component Avis de 04-02 amb to error; mostrar un títol clar; explicar què segueix funcionant; oferir un botó «Tornar-ho a intentar» i un enllaç al catàleg; mostrar un identificador curt d'incidència generat amb crypto.randomUUID().slice(0, 8); i ser a prova de fallades (no ha de llançar encara que error sigui null). Explica on s'ha de generar l'identificador i per què no es pot generar al cos del component.
Solucions
Solució 1.
// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import LimitError from './components/LimitError.jsx';
import { registrarError } from './utilitats/monitoritzacio.js';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<LimitError
titol="CicloUrbano no està disponible ara mateix"
alRegistrar={(error, pila) => registrarError(error, pila, { seccio: 'global' })}
>
<App />
</LimitError>
</StrictMode>
);// src/App.jsx (fragment del return)
<Disseny>
<LimitError
titol="No hem pogut carregar el catàleg"
alRegistrar={(e, pila) => registrarError(e, pila, { seccio: 'cataleg', tipusTriat })}
>
<SelectorTipus tipusTriat={tipusTriat} alCanviarTipus={gestionarCanviTipus} />
<LlistaBicicletes
bicicletes={bicicletesVisibles}
estacions={estacions}
alSeleccionar={gestionarSeleccioBicicleta}
alReservar={gestionarReserva}
/>
</LimitError>
<LimitError
titol="No hem pogut carregar les teves reserves"
alRegistrar={(e, pila) => registrarError(e, pila, { seccio: 'reserves' })}
>
<PanellReserva
bicicleta={bicicletaSeleccionada}
hores={horesReserva}
alCanviarHores={gestionarCanviHores}
alConfirmar={gestionarConfirmacio}
/>
</LimitError>
<LimitError
titol="No hem pogut carregar les estacions"
alRegistrar={(e, pila) => registrarError(e, pila, { seccio: 'estacions' })}
>
<LlistaEstacions estacions={estacions} />
</LimitError>
</Disseny>Capcalera i PeuDePagina queden fora per dos motius. Primer, són marcatge pràcticament estàtic: no consumeixen dades externes ni fan càlculs sobre camps que puguin faltar, de manera que la seva probabilitat de fallada és mínima. I segon, si tot i així fallessin, perdre la navegació deixa l'aplicació inutilitzable: aquí no interessa una alternativa local, sinó que la fallada pugi fins al límit global i es mostri un missatge d'aplicació completa. Un límit es col·loca on té sentit continuar usant la resta.
Solució 2.
| Cas | El captura? | Gestió |
|---|---|---|
1. preuHora indefinit al render |
Sí | És el cas canònic. Tot i així, el correcte és prevenir-lo amb bicicleta.preuHora ?? 0 i deixar el límit com a xarxa de seguretat |
2. fetch amb 500 en un gestor |
No | Doble motiu: és un gestor i és asíncron. try/catch amb async/await i un estat errorReserva mostrat amb Avis |
3. setTimeout que llança als dos segons |
No | El callback s'executa fora del context de React. try/catch dins del callback i guardar l'error en estat |
| 4. Constructor d'una classe heretada | Sí | Els constructors dels descendents estan coberts, igual que els seus mètodes de cicle de vida |
| 5. L'alternativa llegeix un camp inexistent | No, no ho captura aquest límit | El límit no es protegeix a si mateix: l'error puja al límit superior (el global). Solució: alternativa defensiva, error?.detalls?.codi ?? 'sense codi' |
Solució 3.
// src/components/AlternativaReserves.jsx
import Avis from './Avis.jsx';
/**
* Interfície de reserva del límit d'error del panell de reserves.
* Props:
* - error (objecte Error, opcional): pot arribar nul
* - alReintentar (funció, obligatori)
* - idIncidencia (cadena, opcional)
*/
function AlternativaReserves({ error, alReintentar, idIncidencia }) {
const detall = error?.message ?? 'Error desconegut';
return (
<Avis
to="error"
titol="No hem pogut carregar les teves reserves"
accions={
<>
<button type="button" onClick={alReintentar}>Tornar-ho a intentar</button>
<a href="/catalogo">Veure el catàleg</a>
</>
}
>
<p>
Hi ha hagut un problema amb el panell de reserves. El catàleg de bicicletes i
les estacions segueixen funcionant amb normalitat.
</p>
{idIncidencia && (
<p>
Referència per a suport: <code>{idIncidencia}</code>
</p>
)}
<p className="detall-tecnic">{detall}</p>
</Avis>
);
}
export default AlternativaReserves;L'identificador no es pot generar al cos del component per dues raons. La primera és de correcció: crypto.randomUUID() al cos retornaria un valor diferent a cada render, així que la referència que veu la persona usuària canviaria en repintar-se el component i ja no coincidiria amb la que es va enviar al servei de monitorització. La segona és conceptual: generar un valor aleatori durant el render el fa impur, i el render ha de ser pur (04-03).
El lloc correcte és el moment en què es captura l'error, és a dir, dins de componentDidCatch, que s'executa una sola vegada per fallada i sí que admet efectes secundaris:
componentDidCatch(error, infoError) {
const idIncidencia = `inc-${crypto.randomUUID().slice(0, 8)}`;
this.setState({ idIncidencia });
if (this.props.alRegistrar) {
this.props.alRegistrar(error, infoError.componentStack, idIncidencia);
}
}Així, el mateix identificador viatja al servei de monitorització i apareix en pantalla: qui truqui a suport donarà exactament la referència que l'equip pot cercar. És el mateix criteri que segueix FormulariReserva a 03-05 en generar els seus ids res-${crypto.randomUUID().slice(0, 8)} al gestor d'enviament i no durant el render.
Conclusió
Un error llançat durant el render desmunta l'arbre sencer de React i deixa una pantalla en blanc. Els límits d'error són la resposta: components que capturen les fallades del seu subarbre i pinten una interfície alternativa. S'implementen amb dos mètodes que avui dia només existeixen en components de classe —static getDerivedStateFromError per decidir què pintar i componentDidCatch per registrar—, i aquesta és l'única excepció que queda a la regla que tot s'escriu amb funcions i hooks. Has construït LimitError per a CicloUrbano amb alternativa personalitzable i botó de reintent, l'has col·locat en dos nivells —un de global a main.jsx i diversos de granulars per secció— i saps que no capturen gestors d'esdeveniments, codi asíncron ni les seves pròpies fallades, amb la tècnica adequada per a cada cas. Afegeix-hi el registre en un servei de monitorització amb context de negoci, react-error-boundary com a opció pràctica per al dia a dia, i uns missatges de fallada que expliquen, delimiten i ofereixen una sortida.
Amb això es tanca el Mòdul 4. En quatre lliçons el projecte ha canviat d'escala: l'estat va pujar a l'avantpassat comú i el catàleg filtra de debò (04-01); la interfície es va recompondre amb contenidors, espais amb nom i especialitzacions en lloc d'herència (04-02); vas aprendre a llegir el cicle de vida clàssic i a traduir-lo (04-03); i vas muntar el mapa complet dels hooks amb les regles que els governen (04-04).
Ara toca recórrer aquest mapa amb detall. El Mòdul 5: Hooks de React dedica una lliçó a cada eina: useState a fons amb tots els seus matisos, useEffect i el model de sincronització que es va anunciar a 04-03, useRef per al que es recorda sense repintar, useContext per acabar amb la perforació de props que va quedar pendent a 04-01, useReducer per a l'estat complex i, finalment, els hooks personalitzats, on la teva pròpia lògica es converteix en una funció reutilitzable sense cap embolcall. La propera lliçó és Hook useState.
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
