Tota la navegació de CicloUrbano neix fins ara d'un clic de l'usuari sobre un <Link>. Però moltes navegacions no són un clic sobre un enllaç: quan l'usuari confirma el FormulariReserva a /reservas/nueva, l'aplicació ha de crear la reserva i portar-lo a /reservas, sense que el formulari quedi a l'historial esperant que premi «enrere» i el reenviï; quan una operació acaba, convé tornar a la pantalla anterior; quan algú intenta abandonar un formulari a mitges, convé aturar-lo i preguntar. Tot això és navegació programàtica: navegar des del codi. En aquesta lliçó aprendràs useNavigate amb les seves opcions, com passar i llegir estat a la navegació, el useLocation complet, l'alternativa declarativa <Navigate />, com bloquejar una sortida amb useBlocker, com restaurar la posició de desplaçament amb ScrollRestoration i com mostrar un indicador de càrrega amb useNavigation.
Contingut
- Quan navegar des del codi i quan no
useNavigate: la signatura completareplace: quan no deixar rastre a l'historial- Navegar cap enrere i cap endavant amb números
- Passar estat a la navegació
useLocationcomplet- Cas central: confirmar una reserva
- Redireccions declaratives amb
<Navigate /> <Navigate />enfront deuseNavigateen un efecte- Bloquejar la sortida amb
useBlocker - Restaurar el desplaçament amb
ScrollRestoration - Indicadors de càrrega amb
useNavigation
- Quan navegar des del codi i quan no
Abans de l'API, el criteri, perquè l'error més comú d'aquest tema no és tècnic sinó de disseny.
Si l'usuari decideix anar a un lloc, això és un enllaç. Si l'aplicació decideix portar-lo, això és navegació programàtica.
| Situació | Eina correcta |
|---|---|
| «Veure fitxa de la bicicleta» | <Link to={...}> |
| Menú de la capçalera | <NavLink to={...}> |
| Pestanyes d'una estació | <NavLink to="incidencias"> |
| Després de confirmar una reserva, anar a «Les meves reserves» | useNavigate |
| Després d'identificar-se, tornar a la destinació original | useNavigate |
Si no hi ha sessió, portar a /acceso |
<Navigate /> |
| Botó «Cancel·lar» que torna enrere | useNavigate(-1) |
| Després de 5 segons d'inactivitat, tancar sessió | useNavigate |
I l'antipatró que cal evitar per damunt de tot:
{/* ❌ NO facis MAI això */}
<div onClick={() => navegar('/estaciones')}>Estacions</div>
<button type="button" onClick={() => navegar('/estaciones')}>Estacions</button>Tots dos «funcionen» i tots dos són un fallo d'accessibilitat greu, per les raons de 03-06: no es poden obrir en pestanya nova, no es pot copiar l'adreça, un lector de pantalla els anuncia com a «botó» o directament com a res, els cercadors no els segueixen i el <div> ni tan sols és accessible amb el tabulador. Un <Link> produeix un <a> de veritat i conserva tot això. Si la destinació es coneix en el moment de renderitzar, és un enllaç.
useNavigate: la signatura completa
useNavigate: la signatura completaimport { useNavigate } from 'react-router';
function ElMeuComponent() {
const navegar = useNavigate();
// …
}El hook retorna una funció amb dues formes de crida:
// Forma 1: navegar a una ruta
navegar(desti, opcions);
// Forma 2: moure's per l'historial
navegar(delta); // número: -1 enrere, 1 endavant, -2 dos enrere…Les opcions de la primera forma:
| Opció | Tipus | Què fa |
|---|---|---|
replace |
booleà | Substitueix l'entrada actual de l'historial en lloc d'afegir-ne una |
state |
qualsevol cosa serialitzable | Dades que viatgen amb la navegació, invisibles a la URL |
relative |
'route' | 'path' |
Com interpretar una destinació relativa (com a <Link>, 06-03) |
preventScrollReset |
booleà | Impedeix que el desplaçament torni amunt |
flushSync |
booleà | Força l'actualització síncrona del DOM. Cas rar |
I la destinació admet tant una cadena com un objecte, útil quan vols construir la consulta per parts:
navegar('/reservas'); // cadena simple
navegar(`/bicicletas/${bicicleta.id}`); // interpolada
navegar({ pathname: '/', search: '?tipo=electrica' }); // objecte
navegar('incidencias'); // relativa a la ruta activa
navegar('..'); // puja un nivell de rutaUna regla que no es pot saltar: navegar canvia l'estat de l'enrutador, així que només es pot cridar durant un esdeveniment o dins d'un efecte, mai durant el render.
function PaginaTaller() {
const navegar = useNavigate();
const { esOperari } = useUsuari();
if (!esOperari) {
navegar('/acceso'); // ❌ Warning: Cannot update a component while rendering another
}
// …
}Aquest error és dels primers que veuràs. Durant el render, un component no pot provocar una actualització d'un altre. Les dues sortides correctes estan als apartats 8 i 9.
replace: quan no deixar rastre a l'historial
replace: quan no deixar rastre a l'historialPer defecte, navegar('/reservas') apila una entrada nova a l'historial, igual que un <Link>. Amb { replace: true } substitueix l'actual.
Compara els dos historials després de crear una reserva:
flowchart LR
subgraph SIN["Sense replace"]
A1["/"] --> B1["/reservas/nueva"] --> C1["/reservas"]
end
subgraph CON["Amb replace: true"]
A2["/"] --> C2["/reservas"]
end
style B1 fill:#fecaca
Estant a /reservas i prement «enrere»:
Sense replace |
Amb replace: true |
|
|---|---|---|
| Destinació d'«enrere» | /reservas/nueva |
/ |
| Què veu l'usuari | El formulari una altra vegada, potser amb les dades que ja va enviar | El catàleg |
| Risc | Que torni a enviar la mateixa reserva | Cap |
Aquest risc és real i té nom propi: l'usuari veu el formulari, pensa que no s'ha desat, torna a omplir-lo i acaba amb dues reserves idèntiques. És exactament el problema que a les webs tradicionals resol el patró POST-Redirect-GET, i replace n'és l'equivalent en una SPA.
Quan usar replace: true:
- Després d'enviar un formulari amb èxit. El formulari ja no ha d'estar a l'historial.
- Després d'iniciar sessió. «Enrere» no ha de tornar al formulari d'accés d'un usuari ja identificat.
- En una redirecció. Una pantalla que només redirigeix no ha de quedar a l'historial, o «enrere» rebotaria en bucle.
- En corregir la URL. Si converteixes
/bicicletas/BICI-003en/bicicletas/bici-003, substitueix. - En filtres i ordenacions (ja ho vas aplicar a 06-02 amb
useSearchParams).
Quan NO usar-lo:
- Navegació normal entre seccions. L'usuari espera poder tornar.
- En obrir un detall des d'una llista. «Enrere» ha de tornar a la llista.
- En canviar de pestanya dins d'una pantalla, si consideres que són estats navegables.
- Navegar cap enrere i cap endavant amb números
navegar(-1); // enrere, com el botó del navegador
navegar(1); // endavant
navegar(-2); // dues pantalles enrere
navegar(0); // recarrega la ruta actual (rar, però existeix)És la forma natural d'un botó «Cancel·lar» o «Tornar»:
// src/components/BotoTornar.jsx
import { useNavigate } from 'react-router';
/**
* Botó de retrocés.
* Props:
* - destinacioAlternativa (cadena, opcional, per defecte '/'): on anar si no hi ha historial
* - children (contingut, opcional)
*/
function BotoTornar({ destinacioAlternativa = '/', children = 'Tornar' }) {
const navegar = useNavigate();
function gestionarClic() {
// window.history.length > 2 ≈ hi ha on tornar dins de l'aplicació
if (window.history.length > 2) {
navegar(-1);
} else {
navegar(destinacioAlternativa, { replace: true });
}
}
return (
<button type="button" onClick={gestionarClic}>
{children}
</button>
);
}
export default BotoTornar;La raó de l'if és una limitació honesta que convé conèixer: navegar(-1) no pot saber on va. Si l'usuari ha arribat a /reservas/nueva enganxant la URL directament, no hi ha entrada anterior dins de la teva aplicació i -1 el traurà de CicloUrbano, potser de tornada al seu cercador. I window.history.length és només una aproximació, perquè compta tot l'historial de la pestanya, no només el de la teva aplicació; no existeix una manera fiable de saber-ho, per raons de privacitat.
Per això, per a un botó «Tornar a les estacions» amb destinació coneguda, és preferible un <Link to="/estaciones">: és determinista, accessible i es pot obrir en pestanya nova. Reserva navegar(-1) per al «Cancel·lar» genèric d'un diàleg o un formulari al qual es pot arribar des de diversos llocs.
- Passar estat a la navegació
A vegades la pantalla de destinació necessita saber alguna cosa sobre com s'hi ha arribat. El cas de CicloUrbano: després de crear una reserva vols que /reservas mostri l'AvisReservaCreada, però només si véns de crear-la; no cada vegada que hi entres.
L'opció state transporta dades que no apareixen a la URL:
I a la destinació es llegeix amb useLocation:
Aquest mecanisme es recolza directament en el history.pushState que vas veure a 06-01, amb les conseqüències que això implica:
| Característica | Detall |
|---|---|
| No es veu a la URL | Bé per a dades accessòries, malament per a res que hagi de ser compartible |
| Sobreviu a la recàrrega | El navegador el guarda amb l'entrada de l'historial |
| Es perd en obrir la URL en pestanya nova | No forma part de l'adreça |
| Ha de ser serialitzable | Res de funcions, Map, Set, elements del DOM o classes |
| Té límit de mida | Uns quants megabytes; no és un magatzem de dades |
| És visible per a l'usuari | history.state a la consola el mostra: no hi posis mai secrets |
I la regla de disseny que evita el mal ús: state és per al «com hi he arribat», no per al «què estic veient». L'avís de reserva creada, la destinació original abans d'un inici de sessió o «vinc del llistat filtrat» són bons usos. La bicicleta que s'està mostrant no: això va a la URL, perquè ha de ser compartible i sobreviure a una pestanya nova.
useLocation complet
useLocation completuseLocation() retorna un objecte amb la URL actual descomposta, i repinta el component cada vegada que canvia.
import { useLocation } from 'react-router';
function Depurador() {
const location = useLocation();
console.log(location);
return null;
}Per a la URL /estaciones/est-02/incidencias?orden=fecha#nota-3:
{
pathname: '/estaciones/est-02/incidencias',
search: '?orden=fecha',
hash: '#nota-3',
state: null,
key: 'x7k2m9'
}| Propietat | Contingut | Ús típic |
|---|---|---|
pathname |
El camí, sense consulta ni fragment | Decidir què està actiu, registrar la pàgina vista |
search |
La cadena de consulta amb el seu ? |
Normalment prefereixes useSearchParams (06-02) |
hash |
El fragment amb el seu # |
Desplaçar-se a una secció concreta |
state |
El que s'ha passat a la navegació | Avisos, destinació original |
key |
Identificador únic d'aquesta entrada de l'historial | Reiniciar estat, cachés per entrada |
Sobre key, que és la menys coneguda i la més útil de les tres últimes: canvia amb cada navegació, fins i tot si tornes a la mateixa URL. Serveix per forçar el reinici d'un subarbre, amb el mateix mecanisme de les claus de 03-03:
// Cada vegada que es navega, el formulari es remunta des de zero
const { key } = useLocation();
<FormulariReserva key={key} />Un exemple complet de registre de pàgines vistes, que en una aplicació real anirà a l'eina d'analítica:
// src/hooks/useRegistreDeNavegacio.js
import { useEffect } from 'react';
import { useLocation } from 'react-router';
/**
* Registra cada canvi de pantalla. Sense paràmetres. No retorna res.
*/
export function useRegistreDeNavegacio() {
const { pathname, search } = useLocation();
useEffect(() => {
// En producció, aquí aniria la crida a l'eina de mesurament
console.info('[navegació] pantalla vista:', pathname + search);
}, [pathname, search]);
}Fixa't en les dependències: pathname i search, no l'objecte location sencer. React Router retorna un objecte nou a cada navegació, però desestructurar cadenes fa que l'efecte només es dispari quan canvien de veritat, evitant el problema de dependències de 05-02. Cridat des de Disseny, cobreix tota l'aplicació.
- Cas central: confirmar una reserva
Reunim tot l'anterior en el flux més important de CicloUrbano. A /reservas/nueva l'usuari omple el FormulariReserva; en confirmar, l'aplicació valida, construeix la reserva, la despatxa al reductorReserves i el porta a /reservas amb un avís.
sequenceDiagram
participant U as Usuari
participant F as PaginaNovaReserva
participant V as validarReserva
participant R as reductorReserves
participant N as useNavigate
U->>F: Envia el formulari
F->>V: validarReserva(esborrany, bicicletes)
alt Hi ha errors
V-->>F: { bicicletaId: '…' }
F-->>U: Missatges als camps (03-05)
else Vàlid
V-->>F: {}
F->>R: despatxar({ tipus: 'reserva_creada', reserva })
F->>N: navegar('/reservas', { replace: true, state: {…} })
N-->>U: Pantalla «Les meves reserves» amb l'avís
end
// src/pagines/PaginaNovaReserva.jsx
import { useNavigate } from 'react-router';
import { useReserves } from '../contextos/ContextReserves.jsx';
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import { validarReserva } from '../utilitats/validarReserva.js';
import FormulariReserva from '../components/FormulariReserva.jsx';
import { bicicletes } from '../dades/domini.js';
function PaginaNovaReserva() {
const navegar = useNavigate();
const { estat, despatxar } = useReserves();
const { usuari } = useUsuari();
function gestionarEnviament(esborrany) {
const errors = validarReserva(esborrany, bicicletes);
if (Object.keys(errors).length > 0) {
return; // el propi formulari ja mostra els missatges (03-05)
}
despatxar({ tipus: 'enviament_iniciat' });
// Les dades no deterministes es generen aquí, no al reductor (05-05)
const reserva = {
id: `res-${crypto.randomUUID().slice(0, 8)}`,
bicicletaId: esborrany.bicicletaId,
usuari: usuari.id,
dataInici: esborrany.dataInici,
hores: esborrany.hores,
estat: 'activa'
};
despatxar({ tipus: 'reserva_creada', reserva });
// replace: el formulari no ha de quedar a l'historial
navegar('/reservas', {
replace: true,
state: { avisReservaCreada: true, idReserva: reserva.id }
});
}
return (
<section>
<h2>Nova reserva</h2>
<FormulariReserva
bicicletes={bicicletes}
esborrany={estat.esborrany}
alCanviarCamp={(camp, valor) =>
despatxar({ tipus: 'esborrany_actualitzat', camp, valor })
}
alEnviar={gestionarEnviament}
/>
</section>
);
}
export default PaginaNovaReserva;I la destinació, que mostra l'avís una sola vegada:
// src/pagines/PaginaReserves.jsx
import { useEffect } from 'react';
import { useLocation, useNavigate, Link } from 'react-router';
import { useReserves } from '../contextos/ContextReserves.jsx';
import { useAvisos } from '../contextos/ContextAvisos.jsx';
import PanellReserves from '../components/PanellReserves.jsx';
function PaginaReserves() {
const { estat } = useReserves();
const { mostrarAvis } = useAvisos();
const location = useLocation();
const navegar = useNavigate();
useEffect(() => {
if (!location.state?.avisReservaCreada) return;
mostrarAvis({
to: 'exit',
titol: 'Reserva creada',
text: `La teva reserva ${location.state.idReserva} està confirmada.`
});
// Es consumeix el state perquè no reaparegui en recarregar (F5)
navegar(location.pathname, { replace: true, state: null });
}, [location.state, location.pathname, mostrarAvis, navegar]);
return (
<section>
<h2>Les meves reserves</h2>
{estat.reserves.length === 0 ? (
<p>
Encara no tens reserves. <Link to="/reservas/nueva">Crear-ne una</Link>
</p>
) : (
<PanellReserves reserves={estat.reserves} />
)}
</section>
);
}
export default PaginaReserves;Tres decisions que mereixen justificació:
despatxarabans denavegar. L'ordre importa poc a la pràctica, perquè React agrupa les actualitzacions (05-01) iProveidorReservesviu aDisseny, que no es desmunta (06-03), però llegir-ho en aquest ordre expressa la intenció: primer es desa la dada, després es canvia de pantalla.- Es consumeix el
state. Sense aquestnavegar(pathname, { state: null }), un F5 a/reservastornaria a mostrar «Reserva creada», perquè elstatesobreviu a la recàrrega. Se substitueix l'entrada per una d'idèntica sensestate, i l'avís no torna. replace: trueen totes dues navegacions. A la primera, per treure el formulari de l'historial. A la segona, per no afegir una entrada duplicada de/reservascada vegada que es neteja l'avís.
- Redireccions declaratives amb
<Navigate />
<Navigate /><Navigate /> és un component que, en renderitzar-se, navega. No pinta res.
import { Navigate } from 'react-router';
function PaginaTaller() {
const { esOperari } = useUsuari();
if (!esOperari) {
return <Navigate to="/acceso" replace />;
}
return <PanellDeTaller />;
}Les seves props són les mateixes opcions de useNavigate:
| Prop | Equival a |
|---|---|
to |
La destinació |
replace |
{ replace: true } |
state |
{ state: … } |
relative |
{ relative: … } |
L'avantatge enfront de useNavigate al render és que és legal: <Navigate /> no navega durant el render de qui el retorna, sinó en el seu propi efecte de muntatge. Per això no produeix l'avís de «Cannot update a component while rendering another» de l'apartat 2.
<Navigate /> enfront de useNavigate en un efecte
<Navigate /> enfront de useNavigate en un efecteTots dos resolen «portar l'usuari a un altre lloc sense que hagi premut res», i triar bé evita força problemes:
<Navigate to replace /> |
useNavigate dins de useEffect |
|
|---|---|---|
| On s'escriu | Al return, com qualsevol JSX |
En un efecte |
| Quan actua | En renderitzar-se | Després del render, quan canvien les seves dependències |
| Llegibilitat | Alta: es veu a l'arbre | Mitjana: cal llegir les dependències |
| Condició basada en props/estat | ✅ Natural | ✅ Possible |
| Reaccionar a alguna cosa asíncrona (una promesa, un temporitzador) | ❌ No | ✅ Sí |
| Risc de bucle | Baix | Alt si falta la condició |
| Recomanació | Per defecte | Només quan la decisió no es pot prendre al render |
Regla pràctica: si ho pots decidir mirant props i estat durant el render, usa <Navigate />. És el que farà RutaProtegida a 06-05. Deixa l'efecte per quan la decisió depengui d'alguna cosa que passa després:
// Cas legítim de navegació en efecte: tancament de sessió per inactivitat
useEffect(() => {
const temporitzador = setTimeout(() => {
tancarSessio();
navegar('/acceso', { replace: true, state: { motiu: 'inactivitat' } });
}, 15 * 60 * 1000);
return () => clearTimeout(temporitzador); // neteja obligatòria (05-02)
}, [tancarSessio, navegar]);L'avís important: navegar dins d'un efecte sense condició provoca un bucle.
Recorregut del desastre: el component es munta, l'efecte navega a /reservas, la ruta canvia, la pantalla es munta, l'efecte torna a navegar… La pestanya es congela i l'historial s'omple d'entrades. Amb { replace: true } l'historial no creix, però el bucle continua.
Les tres regles que ho eviten:
- Sempre una condició que deixi de complir-se després de navegar:
if (!usuari) navegar('/acceso'). - Dependències completes i estables.
navegarés estable entre renders, així que pot anar a l'array sense perill. - Comprova on ets. Si la destinació pot coincidir amb la ruta actual,
if (location.pathname !== desti)abans de navegar.
- Bloquejar la sortida amb
useBlocker
useBlockerSituació coneguda: l'usuari ha omplert mitja reserva i prem «Estacions» al menú. Si el deixes anar, perd la feina sense cap avís.
useBlocker —disponible només en el mode de dades, una altra raó de l'elecció de 06-01— permet interceptar una navegació en curs i decidir si deixar-la passar.
El bloquejador és un objecte amb un estat:
bloquejador.state |
Significat |
|---|---|
'unblocked' |
No hi ha res bloquejat |
'blocked' |
S'ha interceptat una navegació i espera decisió |
'proceeding' |
S'ha cridat proceed() i la navegació s'està completant |
I dos mètodes, disponibles quan està 'blocked':
bloquejador.proceed(): deixa continuar la navegació interceptada.bloquejador.reset(): la cancel·la; l'usuari es queda on era.
A més, bloquejador.location conté la destinació que s'intentava assolir, útil per dir on anava.
// src/hooks/useBloquejarSortida.js
import { useBlocker } from 'react-router';
/**
* Intercepta la sortida d'una pantalla quan hi ha canvis sense desar.
* Paràmetres:
* - hiHaCanvisSenseDesar (booleà)
* Retorna: l'objecte bloquejador de React Router
*/
export function useBloquejarSortida(hiHaCanvisSenseDesar) {
return useBlocker(
({ currentLocation, nextLocation }) =>
hiHaCanvisSenseDesar && currentLocation.pathname !== nextLocation.pathname
);
}La comparació de pathname evita bloquejar quan només canvia la cadena de consulta: si l'usuari ajusta un filtre dins de la mateixa pantalla, no té sentit preguntar-li si vol abandonar-la.
I la interfície de confirmació, reutilitzant el Modal del projecte:
// src/components/DialegSortida.jsx
import Modal from './Modal.jsx';
/**
* Diàleg de confirmació de sortida amb canvis sense desar.
* Props:
* - bloquejador (objecte, obligatori): el que retorna useBlocker
*/
function DialegSortida({ bloquejador }) {
if (bloquejador.state !== 'blocked') return null;
return (
<Modal
titol="Tens una reserva sense acabar"
alTancar={() => bloquejador.reset()}
>
<p>
Si surts ara perdràs les dades que has introduït. Vols sortir de
totes maneres?
</p>
<p>
<button type="button" onClick={() => bloquejador.reset()}>
Continuar editant
</button>{' '}
<button type="button" onClick={() => bloquejador.proceed()}>
Sortir sense desar
</button>
</p>
</Modal>
);
}
export default DialegSortida;Ús a la pantalla de nova reserva:
// src/pagines/PaginaNovaReserva.jsx — afegits
import { useBloquejarSortida } from '../hooks/useBloquejarSortida.js';
import DialegSortida from '../components/DialegSortida.jsx';
function PaginaNovaReserva() {
const { estat, despatxar } = useReserves();
// …
// Hi ha canvis si l'esborrany difereix de buit i encara no s'ha enviat
const hiHaCanvis =
estat.estatEnviament !== 'enviat' &&
(estat.esborrany.bicicletaId !== '' || estat.esborrany.dataInici !== '');
const bloquejador = useBloquejarSortida(hiHaCanvis);
return (
<section>
<h2>Nova reserva</h2>
<FormulariReserva /* … */ />
<DialegSortida bloquejador={bloquejador} />
</section>
);
}El que useBlocker no cobreix, i convé tenir present: només intercepta les navegacions de React Router. Tancar la pestanya, recarregar amb F5 o escriure una altra adreça a la barra no passen per l'enrutador. Per a aquests casos hi ha un mecanisme del navegador, molt més bast:
useEffect(() => {
if (!hiHaCanvis) return;
function gestionarSortida(esdeveniment) {
esdeveniment.preventDefault();
// El navegador mostra EL SEU missatge: no es pot personalitzar
esdeveniment.returnValue = '';
}
window.addEventListener('beforeunload', gestionarSortida);
return () => window.removeEventListener('beforeunload', gestionarSortida);
}, [hiHaCanvis]);I un avís de disseny: bloquejar la sortida és intrusiu. Usa-ho només quan la pèrdua sigui real i costosa —un formulari llarg, un editor—, mai amb un camp de cerca a mitges. I sempre amb la condició ben afinada: un diàleg que apareix quan l'usuari no ha tocat res és de les coses que més molesten d'una aplicació.
- Restaurar el desplaçament amb
ScrollRestoration
ScrollRestorationProva això en qualsevol SPA sense configurar: baixa fins al final d'una llista llarga, entra en un detall, torna enrere. Apareixeràs del tot amunt, i hauràs de tornar a baixar per trobar on eres.
La causa és la que ja coneixes de 06-01: en navegar no hi ha recàrrega de pàgina. La restauració automàtica del desplaçament que fa el navegador amb el botó enrere està lligada al cicle de càrrega de documents, i en una SPA aquest cicle no passa. El DOM se substitueix sense que el navegador consideri que hi ha hagut cap navegació.
React Router ho resol amb un component que es col·loca una vegada, a Disseny:
// src/components/Disseny.jsx — versió final del mòdul
import { Outlet, ScrollRestoration } from 'react-router';
// …
function Disseny() {
return (
<ProveidorReserves>
<ProveidorAvisos>
<div className={estils.disseny}>
<Capcalera />
<LlistaAvisos />
<main className={estils.principal}>
<MollesDePa />
<Outlet />
</main>
<PeuDePagina />
<ScrollRestoration />
</div>
</ProveidorAvisos>
</ProveidorReserves>
);
}El seu comportament per defecte:
- En una navegació nova (
<Link>,navigate), desplaça amunt. - En una navegació de tornada (enrere/endavant), restaura la posició que tenia aquella entrada de l'historial.
Es pot afinar amb getKey, que decideix sota quina clau es desa cada posició:
<ScrollRestoration
getKey={(location) => {
// El catàleg comparteix posició encara que canviï el filtre:
// tornar d'una fitxa retorna al mateix punt de la llista
if (location.pathname === '/') return location.pathname;
// La resta, una posició per entrada de l'historial (comportament normal)
return location.key;
}}
/>I per a casos puntuals en què no vols que el desplaçament salti amunt —canviar de pestanya dins d'una pantalla llarga, per exemple—, tant <Link> com navigate accepten preventScrollReset:
<NavLink to="incidencias" preventScrollReset>Incidències</NavLink>
navegar('?tipo=carga', { preventScrollReset: true });ScrollRestoration és, juntament amb useBlocker, una altra API exclusiva del mode de dades.
- Indicadors de càrrega amb
useNavigation
useNavigationA CicloUrbano les navegacions són instantànies perquè les dades estan en memòria. Tan bon punt una ruta carregui dades d'una API o el seu codi es descarregui de manera mandrosa (08-04), hi haurà un interval en què l'usuari ja ha premut i encara no veu res. Sense senyal visible, tornarà a prémer.
useNavigation informa de l'estat global de la navegació:
navegacio.state |
Significat |
|---|---|
'idle' |
No hi ha cap navegació en curs |
'loading' |
S'està navegant: carregant el codi o les dades de la ruta destinació |
'submitting' |
S'està enviant un formulari a un action |
I camps auxiliars: navegacio.location (on es va), navegacio.formData (el que s'envia).
Una barra de progrés discreta a Disseny:
// src/components/BarraDeProgres.jsx
import { useNavigation } from 'react-router';
import estils from './BarraDeProgres.module.css';
function BarraDeProgres() {
const navegacio = useNavigation();
const carregant = navegacio.state !== 'idle';
if (!carregant) return null;
return (
<div
className={estils.barra}
role="progressbar"
aria-label="Carregant la pàgina"
aria-busy="true"
/>
);
}
export default BarraDeProgres;/* src/components/BarraDeProgres.module.css */
.barra {
position: fixed;
top: 0;
left: 0;
height: 3px;
width: 100%;
background: var(--color-marca);
animation: avancar 1.2s ease-in-out infinite;
transform-origin: left;
}
@keyframes avancar {
0% { transform: scaleX(0); }
50% { transform: scaleX(0.7); }
100% { transform: scaleX(1); }
}
/* Respecta la preferència de moviment reduït (03-06) */
@media (prefers-reduced-motion: reduce) {
.barra { animation: none; opacity: 0.8; }
}Un detall d'experiència d'usuari que val la pena aplicar: una barra que apareix i desapareix en 80 mil·lisegons produeix un parpelleig més molest que la mateixa espera. Retarda'n l'aparició:
function BarraDeProgres() {
const navegacio = useNavigation();
const [visible, setVisible] = useState(false);
useEffect(() => {
if (navegacio.state === 'idle') {
setVisible(false);
return;
}
// Només es mostra si l'espera supera els 200 ms
const temporitzador = setTimeout(() => setVisible(true), 200);
return () => clearTimeout(temporitzador);
}, [navegacio.state]);
if (!visible) return null;
return <div className={estils.barra} role="progressbar" aria-busy="true" />;
}I useNavigation també serveix per a un ús local, deshabilitant el botó d'enviament mentre es processa:
const navegacio = useNavigation();
const enviant = navegacio.state === 'submitting';
<button type="submit" disabled={enviant}>
{enviant ? 'Creant reserva…' : 'Confirmar reserva'}
</button>Errors Comuns i Consells
Cridar navegar durant el render. Produeix «Cannot update a component while rendering another». Només dins de gestors d'esdeveniments o d'efectes; per al render, <Navigate />.
Navegar en un efecte sense condició. Bucle infinit garantit. Sempre una condició que deixi de complir-se després de navegar.
Oblidar replace després d'enviar un formulari. «Enrere» torna al formulari i l'usuari el reenvia. Reserves duplicades.
Usar navigate(-1) quan la destinació es coneix. <Link to="/estaciones"> és determinista, accessible i obrible en pestanya nova. -1 pot treure l'usuari de l'aplicació si hi ha arribat enganxant la URL.
Navegar amb onClick en un <div> o en un <button> quan hauria de ser un enllaç. Es perd tot el que un <a> dona gratis: pestanya nova, copiar adreça, teclat, lectors de pantalla, cercadors.
Ficar dades importants a state. Es perden en obrir la URL en una altra pestanya i no es poden compartir. Si la dada defineix el que es veu, va a la URL.
No consumir el state després d'usar-lo. L'avís «Reserva creada» reapareix a cada F5, perquè el state sobreviu a la recàrrega. Substitueix l'entrada amb state: null.
Posar location sencer com a dependència d'un efecte. És un objecte nou a cada navegació. Desestructura pathname i search.
Bloquejar la sortida amb condicions massa àmplies. Un diàleg que apareix sense que l'usuari hagi tocat res és pitjor que no tenir-lo.
Consell: centralitza les rutes en constants. RUTES.reserves en lloc de '/reservas' repartit per vint fitxers fa que renombrar una secció sigui canviar una línia:
// src/rutes-constants.js
export const RUTES = {
cataleg: '/',
fitxaBicicleta: (id) => `/bicicletas/${id}`,
estacions: '/estaciones',
detallEstacio: (id) => `/estaciones/${id}`,
reserves: '/reservas',
novaReserva: '/reservas/nueva',
acces: '/acceso',
taller: '/taller'
};Consell: pregunta't sempre què passa en prémer «enrere». És la comprovació que descobreix gairebé tots els fallats de navegació d'una aplicació, i la que ningú fa fins que un usuari es queixa.
Exercicis
Exercici 1: enviar una bicicleta al taller
TargetaBicicleta té la prop alEnviarATaller, visible només per a operaris. Implementa el flux a PaginaFitxaBicicleta: en prémer, es marca la bicicleta com a mantenimiento i es porta l'operari a /taller amb un avís indicant quina bicicleta ha enviat. Decideix raonadament si usar replace i per què.
Exercici 2: filtre amb tornada al catàleg
Un usuari entra a /?tipo=electrica, obre la fitxa de bici-005 i prem l'enllaç «Tornar al catàleg» de la fitxa. Ara mateix aterra a / sense filtre i perd el seu context. Corregeix-ho perquè torni a /?tipo=electrica, usant el que has après sobre state i useLocation. Explica per què aquesta solució té una limitació quan la fitxa s'obre enganxant la URL directament, i quina alternativa hi ha.
Exercici 3: analitzar quatre navegacions
Per a cada fragment, digues si és correcte i, si no ho és, què falla i com s'arregla.
// A
function PaginaReserves() {
const { estat } = useReserves();
const navegar = useNavigate();
if (estat.reserves.length === 0) navegar('/reservas/nueva');
return <PanellReserves reserves={estat.reserves} />;
}
// B
function PaginaAcces() {
const { usuari } = useUsuari();
if (usuari) return <Navigate to="/" replace />;
return <FormulariAcces />;
}
// C
function PaginaFitxaBicicleta() {
const navegar = useNavigate();
const { bicicletaId } = useParams();
useEffect(() => {
if (!bicicletes.some((b) => b.id === bicicletaId)) {
navegar('/', { replace: true });
}
});
// …
}
// D
function BotoCancelar() {
const navegar = useNavigate();
return <a onClick={() => navegar(-1)}>Cancel·lar</a>;
}Solucions
Solució 1
// src/pagines/PaginaFitxaBicicleta.jsx — fragment
import { useNavigate, useParams, Link } from 'react-router';
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import { bicicletes, estacions } from '../dades/domini.js';
import TargetaBicicleta from '../components/TargetaBicicleta.jsx';
import PaginaNoTrobada from './PaginaNoTrobada.jsx';
function PaginaFitxaBicicleta() {
const { bicicletaId } = useParams();
const navegar = useNavigate();
const { esOperari } = useUsuari();
const bicicleta = bicicletes.find((bici) => bici.id === bicicletaId);
if (!bicicleta) {
return <PaginaNoTrobada recurs="bicicleta" identificador={bicicletaId} />;
}
function gestionarEnviamentATaller() {
// En una aplicació real això seria una crida a l'API o un despatx al reductor
bicicleta.estat = 'mantenimiento'; // simplificació didàctica
navegar('/taller', {
state: {
avisBicicletaEnviada: true,
model: bicicleta.model,
idBicicleta: bicicleta.id
}
});
}
return (
<article>
<TargetaBicicleta
bicicleta={bicicleta}
nomEstacio={estacions.find((e) => e.id === bicicleta.estacioId)?.nom}
alEnviarATaller={esOperari ? gestionarEnviamentATaller : undefined}
/>
<Link to="/">Tornar al catàleg</Link>
</article>
);
}Sobre replace: aquí no convé. El raonament: l'operari venia consultant la fitxa de la bicicleta, i aquesta fitxa continua sent una destinació vàlida i útil després de l'operació —de fet, voldrà comprovar que ara apareix en manteniment—. La fitxa no és un formulari que quedi obsolet en enviar-se, així que «enrere» l'ha de tornar a portar-hi. Compara-ho amb el cas de la reserva: allà el formulari de /reservas/nueva sí que queda obsolet i tornar-hi convida a duplicar la reserva.
Solució 2
// src/components/TargetaBicicleta.jsx — l'enllaç desa d'on ve
import { Link, useLocation } from 'react-router';
function TargetaBicicleta({ bicicleta, /* … */ }) {
const location = useLocation();
return (
<article>
<h3>
<Link
to={`/bicicletas/${bicicleta.id}`}
state={{ tornarA: location.pathname + location.search }}
>
{bicicleta.model}
</Link>
</h3>
{/* … */}
</article>
);
}// src/pagines/PaginaFitxaBicicleta.jsx — l'enllaç de tornada ho llegeix
const location = useLocation();
const destinacioTornada = location.state?.tornarA ?? '/';
return (
<article>
{/* … */}
<Link to={destinacioTornada}>Tornar al catàleg</Link>
</article>
);Fixa't que <Link> accepta state igual que navigate, i que es desa pathname + search per no perdre el filtre.
La limitació: si l'usuari obre /bicicletas/bici-005 enganxant la URL o des d'un enllaç compartit, no hi ha state —no ha passat pel catàleg— i el ?? '/' el retorna al catàleg sense filtrar. És correcte i no trenca res, però convé entendre per què passa: el state viatja amb l'entrada de l'historial, no amb l'adreça.
L'alternativa és posar l'origen a la URL: /bicicletas/bici-005?tornarA=%2F%3Ftipo%3Delectrica. Avantatge: sobreviu a compartir l'enllaç i a obrir-lo en una altra pestanya. Inconvenients: embruta una URL que hauria de ser neta i compartible, i cal sanejar el valor —un tornarA que apunti a un altre domini seria un vector de redirecció oberta—. Per a una dada accessòria de navegació com aquesta, state és la tria correcta; per a alguna cosa que hagi de ser compartible, la URL.
Solució 3
A — Incorrecte. Crida navegar durant el render: avís de React i comportament imprevisible. A més és un mal disseny: portar per força a /reservas/nueva qui entra a «Les meves reserves» li impedeix veure que no en té cap. El correcte és mostrar un estat buit amb un enllaç:
if (estat.reserves.length === 0) {
return (
<p>
Encara no tens reserves. <Link to="/reservas/nueva">Crear-ne una</Link>
</p>
);
}B — Correcte. <Navigate /> durant el render és legal, la condició usuari deixa de complir-se després de navegar (no hi ha bucle) i replace és l'apropiat: un usuari ja identificat no ha de tornar al formulari d'accés prement «enrere». Aquest és exactament el patró que usaràs a 06-05.
C — Incorrecte per dos motius. Primer, a l'useEffect li falta l'array de dependències, així que s'executa després de cada render; amb la condició present no arriba a haver-hi bucle infinit, però és un descuit de 05-02 que tan bon punt la condició es relaxi sí que el provocarà. Ha de ser }, [bicicletaId, navegar]);. Segon, i més important, és l'estratègia equivocada: redirigir al catàleg quan l'identificador no existeix esborra la URL problemàtica i deixa l'usuari sense saber què ha passat. Millor la solució de 06-02: comprovar-ho al render i retornar <PaginaNoTrobada recurs="bicicleta" … />, conservant la URL perquè es pugui corregir o reportar.
D — Incorrecte. Un <a> sense href no és un enllaç per al navegador: no és accessible amb el tabulador, no respon a la tecla Intro i un lector de pantalla no l'anuncia. I semànticament tampoc no és un enllaç, perquè no porta a una destinació coneguda: és una acció. El correcte és un botó:
El type="button" és obligatori per la convenció del projecte i per una raó concreta: dins d'un formulari, un <button> sense type és d'enviament i provocaria un enviament accidental.
Conclusió
La navegació programàtica cobreix tot el que no és un clic en un enllaç, i el criteri per triar entre l'una i l'altra és senzill: si la destinació es coneix en renderitzar i qui decideix és l'usuari, és un <Link>; si decideix l'aplicació, és codi. useNavigate retorna una funció que accepta una destinació i unes opcions, o un número per moure's per l'historial, i d'aquestes opcions la més important és replace: substituir l'entrada actual en lloc d'apilar-ne una altra és el correcte després d'enviar un formulari, després d'identificar-se i en qualsevol redirecció, perquè evita que «enrere» torni a una pantalla obsoleta i que l'usuari dupliqui una reserva sense voler-ho. L'opció state transporta dades que no apareixen a la URL —el «com hi he arribat», mai el «què estic veient»—, sobreviu a la recàrrega però no a obrir l'enllaç en una altra pestanya, i convé consumir-lo després d'usar-lo. useLocation et dona la URL descomposta en pathname, search, hash, state i key, amb el detall que key canvia a cada navegació i serveix per forçar el reinici d'un subarbre.
Has implementat el flux central de CicloUrbano: en confirmar el FormulariReserva a /reservas/nueva es valida amb validarReserva, es construeix la reserva amb les seves dades no deterministes fora del reductor, es despatxa reserva_creada a reductorReserves i es navega a /reservas amb replace i un state que dispara l'AvisReservaCreada una sola vegada. Per a les redireccions has vist que <Navigate to replace /> és l'opció per defecte —legal durant el render, llegible a l'arbre— i que useNavigate dins d'un efecte queda per a quan la decisió depèn d'alguna cosa asíncrona, sempre amb una condició que deixi de complir-se després de navegar, perquè sense ella el bucle és immediat. I has afegit tres peces exclusives del mode de dades que eleven força la qualitat de l'aplicació: useBlocker amb els seus estats blocked i proceeding per impedir que es perdi un formulari a mitges —sabent que no cobreix tancar la pestanya, per a la qual cosa cal beforeunload—, ScrollRestoration per tornar l'usuari al punt de la llista on era, alguna cosa que en una SPA no passa només perquè mai no hi ha recàrrega de document, i useNavigation per mostrar un indicador de càrrega quan la navegació trigui, amb el retard que evita el parpelleig.
Queda una peça del mòdul. /taller és al mapa des de 06-02 i avui la pot obrir qualsevol: n'hi ha prou d'escriure l'adreça. CicloUrbano té dos perfils —usr-01 Ana Ribera, client, i usr-02 Marc Solé, operari— i el panell de taller és només per al segon. Cal construir l'inici de sessió a /acceso, un guardià que redirigeixi qui no tingui sessió i sàpiga tornar-lo a la destinació original, autorització per rol amb una pantalla de «sense permisos» diferent de la de «no trobat», i —el més important de tota la lliçó— entendre per què res d'això és seguretat de veritat. La propera lliçó és Rutes Protegides i Control d'Accés.
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
