La ruta /taller és al mapa de CicloUrbano des de 06-02 i avui la pot obrir qualsevol: n'hi ha prou d'escriure l'adreça a la barra del navegador. Només l'hauria de veure Marc Solé, l'operari usr-02, ni Ana Ribera ni un visitant sense sessió. En aquesta lliçó construiràs aquest control d'accés: un inici de sessió fictici a /acceso, un component guardià que redirigeixi qui no tingui sessió i sàpiga tornar-lo després a la destinació original, una ruta de disseny protegida que agrupi diverses pantalles sota un mateix guardià, autorització per rol amb una pantalla de «sense permisos» diferent de la de «no trobat», la gestió de l'estat intermedi mentre es comprova la sessió, i la persistència amb useMagatzemLocal. Però abans de res, l'advertència que governa tota la lliçó i que cal tenir present a cada línia de codi que escriguis.

⚠️ Advertència imprescindible: això no és seguretat

Tot el que facis al client és únicament experiència d'usuari. L'autorització real es comprova SEMPRE al servidor.

No és una recomanació ni una bona pràctica: és un fet sobre com funciona el web. El codi de la teva aplicació es descarrega sencer al navegador de l'usuari, i allà ell n'és el propietari absolut. Pot:

  • Obrir les eines de desenvolupament i canviar qualsevol variable en memòria, inclosa esOperari.
  • Posar un punt d'interrupció al teu guardià i saltar-se'l.
  • Modificar localStorage a mà per posar-se el rol que vulgui.
  • Llegir tot el JavaScript descarregat, incloses les pantalles «protegides», sense necessitat d'accedir-hi.
  • Trucar directament a la teva API amb curl, sense passar per la interfície en absolut.

El que un guardià de rutes aconsegueix de veritat:

El que SÍ fa El que NO fa
Evitar que un usuari legítim vegi una pantalla que no li serveix Impedir que algú decidit la vegi
Redirigir a /acceso qui no ha entrat Protegir les dades que mostra aquesta pantalla
Mostrar una interfície coherent amb el rol Substituir la comprovació del servidor
Evitar errors per dades que no arriben Ocultar el codi de la pantalla

La conseqüència pràctica: cada petició que la pantalla protegida faci a l'API ha de portar les seves credencials, i el servidor ha de comprovar a cadascuna si aquest usuari té permís per a aquesta operació. Si el servidor retorna la llista d'incidències a qualsevol que la demani, la teva RutaProtegida és un adorn. Hi tornarem en tancar la lliçó, perquè és l'única cosa d'aquí que no admet matisos.

Contingut

  1. El model de sessió de CicloUrbano
  2. La pantalla d'accés
  3. Patró 1: component guardià RutaProtegida
  4. Tornar a la destinació original després d'identificar-se
  5. Patró 2: ruta de disseny protegida
  6. Autorització per rol: RequereixRol
  7. 403 i 404: per què són pantalles diferents
  8. L'estat intermedi: carregantSessio
  9. Persistir la sessió amb useMagatzemLocal
  10. Tokens, cookies i el límit de l'emmagatzematge del navegador
  11. Ocultar a la interfície el que no es pot usar
  12. El mapa de rutes definitiu

  1. El model de sessió de CicloUrbano

No cal inventar res: ContextUsuari existeix des de 05-04 amb la forma exacta que necessitem.

// src/contextos/ContextUsuari.jsx — el que ja tens
export function ProveidorUsuari({ children, usuariInicial = null }) {
  const [usuari, setUsuari] = useState(usuariInicial);

  function iniciarSessio(id) {
    const trobat = usuaris.find((u) => u.id === id);
    setUsuari(trobat ?? null);
  }

  function tancarSessio() {
    setUsuari(null);
  }

  const valor = {
    usuari,
    esOperari: usuari?.rol === 'operario',
    iniciarSessio,
    tancarSessio
  };

  return <ContextUsuari value={valor}>{children}</ContextUsuari>;
}

export function useUsuari() {
  const context = useContext(ContextUsuari);
  if (context === null) {
    throw new Error('useUsuari s\'ha d\'utilitzar dins de <ProveidorUsuari>');
  }
  return context;
}

Els dos perfils ficticis del projecte:

Usuari Nom Correu Rol Què pot veure
usr-01 Ana Ribera [email protected] cliente Catàleg, estacions, les seves reserves
usr-02 Marc Solé [email protected] operario Tot l'anterior més /taller

I tres estats possibles de la sessió, que convé distingir bé perquè el tercer és el que més problemes dona:

stateDiagram-v2
    [*] --> Comprovant: arrenca l'aplicació
    Comprovant --> SenseSessio: no hi ha sessió desada
    Comprovant --> AmbSessio: sessió recuperada
    SenseSessio --> AmbSessio: iniciarSessio()
    AmbSessio --> SenseSessio: tancarSessio()
    note right of Comprovant
        Estat intermedi.
        Ni redirigir ni mostrar
        contingut protegit:
        indicador de càrrega.
    end note

Un canvi necessari respecte a 05-04: aleshores usuariInicial era usuaris[0], perquè sempre hi havia algú identificat. Ara el valor inicial és null, perquè «ningú ha entrat» ha de ser un estat representable. Tan bon punt existeix una pantalla d'accés, arrencar amb sessió seria una contradicció.

  1. La pantalla d'accés

Un formulari d'accés fictici, sense contrasenyes, que només tria entre els dos perfils. En una aplicació real hi hauria credencials i una crida a l'API; per aprendre enrutament, això només afegiria soroll.

// src/pagines/PaginaAcces.jsx
import { useState } from 'react';
import { useNavigate, useLocation, Navigate } from 'react-router';
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import { usuaris } from '../dades/domini.js';
import Avis from '../components/Avis.jsx';
import estils from './PaginaAcces.module.css';

function PaginaAcces() {
  const { usuari, iniciarSessio } = useUsuari();
  const navegar = useNavigate();
  const location = useLocation();
  const [idTriat, setIdTriat] = useState('usr-01');

  // On tornar: el guardià ho ha deixat en redirigir aquí (apartat 4)
  const desti = location.state?.tornarA?.pathname ?? '/';

  // Qui ja té sessió no ha de veure el formulari
  if (usuari) {
    return <Navigate to={desti} replace />;
  }

  function gestionarEnviament(esdeveniment) {
    esdeveniment.preventDefault();
    iniciarSessio(idTriat);
    // replace: «enrere» no ha de tornar al formulari d'accés
    navegar(desti, { replace: true });
  }

  return (
    <section className={estils.acces}>
      <h2>Accés a CicloUrbano</h2>

      {location.state?.tornarA && (
        <Avis to="informacio" titol="Necessites identificar-te">
          <p>
            La pantalla <code>{location.state.tornarA.pathname}</code> requereix una
            sessió iniciada. T'hi portarem tan bon punt entris.
          </p>
        </Avis>
      )}

      <form onSubmit={gestionarEnviament}>
        <fieldset>
          <legend>Tria un perfil de demostració</legend>

          {usuaris.map((candidat) => (
            <p key={candidat.id}>
              <label>
                <input
                  type="radio"
                  name="perfil"
                  value={candidat.id}
                  checked={idTriat === candidat.id}
                  onChange={(esdeveniment) => setIdTriat(esdeveniment.target.value)}
                />{' '}
                {candidat.nom} — <span>{candidat.rol}</span>
                <br />
                <small>{candidat.email}</small>
              </label>
            </p>
          ))}
        </fieldset>

        <button type="submit">Entra</button>
      </form>

      <p className={estils.nota}>
        Dades fictícies de demostració. Cap d'aquests comptes existeix ni
        requereix contrasenya.
      </p>
    </section>
  );
}

export default PaginaAcces;

Quatre detalls a destacar:

  • El formulari és controlat (03-04): checked surt de l'estat i onChange l'actualitza. Els botons de ràdio comparteixen name perquè siguin excloents.
  • <fieldset> i <legend> agrupen el conjunt d'opcions i són el que un lector de pantalla anuncia abans de llegir-les, tal com es va veure a 03-06.
  • Qui ja té sessió no veu el formulari: <Navigate to={desti} replace /> durant el render, el patró de 06-04.
  • replace en l'enviament, pel mateix motiu: després d'entrar, «enrere» no ha de tornar a l'accés.

  1. Patró 1: component guardià RutaProtegida

El primer patró embolcalla directament el contingut que cal protegir.

// src/components/RutaProtegida.jsx
import { Navigate, useLocation } from 'react-router';
import { useUsuari } from '../contextos/ContextUsuari.jsx';

/**
 * Guardià de sessió. Renderitza els seus fills només si hi ha un usuari identificat;
 * si no, redirigeix a /acceso recordant la destinació original.
 *
 * ⚠️ Només experiència d'usuari: l'autorització real és del servidor.
 *
 * Props:
 *  - children (contingut, obligatori): el que es protegeix
 */
function RutaProtegida({ children }) {
  const { usuari } = useUsuari();
  const location = useLocation();

  if (!usuari) {
    return <Navigate to="/acceso" replace state={{ tornarA: location }} />;
  }

  return children;
}

export default RutaProtegida;

Ús al mapa:

{
  path: 'taller',
  element: (
    <RutaProtegida>
      <PaginaTaller />
    </RutaProtegida>
  )
}

Les tres decisions d'aquest component, una per una:

<Navigate /> i no useNavigate en un efecte. La decisió es pot prendre mirant l'estat durant el render, així que aplica la regla de 06-04. Amb un efecte hi hauria un instant en què la pantalla protegida es renderitza abans de redirigir: un parpelleig visible del contingut que es volia ocultar, a més del risc de bucle.

replace i no una entrada nova. Sense ell, l'historial quedaria … → /taller → /acceso, i en prémer «enrere» l'usuari tornaria a /taller, que redirigiria de nou a /acceso, que en tornar enrere… Un rebot del qual no se surt. Amb replace, l'entrada de /taller se substitueix i «enrere» porta a on era abans.

state={{ tornarA: location }} desa la destinació original. És l'apartat següent i la diferència entre un control d'accés acceptable i un d'irritant.

  1. Tornar a la destinació original després d'identificar-se

Sense aquesta peça, l'experiència és la següent: un operari rep per xat l'enllaç /taller, l'obre, veu el formulari d'accés, entra… i aterra al catàleg. Ha de tornar al xat i prémer l'enllaç una altra vegada. Amb el state, entra i apareix directament al taller.

sequenceDiagram
    participant U as Usuari
    participant T as /taller
    participant G as RutaProtegida
    participant A as /acceso
    U->>T: Obre l'enllaç compartit
    T->>G: Es renderitza el guardià
    G->>G: usuari === null
    G->>A: Navigate replace<br/>state: { tornarA: { pathname: '/taller' } }
    A-->>U: Formulari + «Necessites identificar-te»
    U->>A: Tria usr-02 i l'envia
    A->>A: iniciarSessio('usr-02')
    A->>T: navegar('/taller', { replace: true })
    T-->>U: Panell de l'operari ✅

Es desa l'objecte location sencer, no només el pathname, per conservar també la consulta:

// Desat pel guardià
state={{ tornarA: location }}

// Llegit a PaginaAcces, reconstruint la URL completa
const tornarA = location.state?.tornarA;
const desti = tornarA ? `${tornarA.pathname}${tornarA.search}` : '/';

Així, qui intentava obrir /taller?filtro=urgentes torna exactament allà.

Una precaució de seguretat que convé conèixer des d'ara. Si la destinació vingués d'un paràmetre de consulta en lloc del state —una cosa habitual en aplicacions que integren un sistema d'accés extern— tindries una redirecció oberta: algú podria enviar \/acceso?tornarA=https://lloc-fals.test, i la teva aplicació portaria l'usuari a un lloc aliè just després d'identificar-se, amb tota l'aparença de legitimitat. La defensa és acceptar només rutes internes:

function destiSegur(candidat) {
  // Ha de començar per una sola barra: ni '//altre.test' ni 'https://…'
  if (typeof candidat !== 'string') return '/';
  if (!candidat.startsWith('/') || candidat.startsWith('//')) return '/';
  return candidat;
}

Amb el state de React Router el risc és molt menor, perquè l'escriu el teu propi guardià i no viatja per la URL, però la comprovació costa quatre línies i convé tenir interioritzat el patró.

  1. Patró 2: ruta de disseny protegida

El patró 1 funciona bé amb una pantalla. Amb quatre, el mapa s'omple d'embolcalls repetits:

// ❌ Repetitiu i fàcil d'oblidar a la cinquena pantalla
{ path: 'taller', element: <RutaProtegida><PaginaTaller /></RutaProtegida> },
{ path: 'informes', element: <RutaProtegida><PaginaInformes /></RutaProtegida> },
{ path: 'flota', element: <RutaProtegida><PaginaFlota /></RutaProtegida> }

Aquí entra la ruta sense path de 06-03: una ruta l'element de la qual és el guardià i que agrupa diverses filles, sense afegir cap segment a la URL.

// src/components/RutaProtegida.jsx — versió per a ruta de disseny
import { Navigate, useLocation, Outlet } from 'react-router';
import { useUsuari } from '../contextos/ContextUsuari.jsx';

function RutaProtegida() {
  const { usuari } = useUsuari();
  const location = useLocation();

  if (!usuari) {
    return <Navigate to="/acceso" replace state={{ tornarA: location }} />;
  }

  return <Outlet />;   // ← en lloc de children
}

export default RutaProtegida;
// src/rutes.jsx — la branca protegida
{
  element: <RutaProtegida />,      // ← sense path: no consumeix segment
  children: [
    { path: 'taller', element: <PaginaTaller /> },
    { path: 'informes', element: <PaginaInformes /> }
  ]
}

Les URL continuen sent /taller i /informes. Comparació dels dos patrons:

Patró 1: embolcallar fills Patró 2: ruta de disseny
Com es declara element: <RutaProtegida><X /></RutaProtegida> Ruta sense path amb children
Què renderitza el guardià children <Outlet />
Amb una pantalla protegida Simple i directe Afegeix un nivell al mapa
Amb diverses Es repeteix a cadascuna Es declara una vegada
Risc d'oblidar protegir-ne una de nova Alt Baix: s'afegeix dins de la branca
Interfície comuna per al grup Cal repetir-la El guardià pot pintar barra lateral, molles…
Recomanació Casos solts Preferible quan hi ha diverses pantalles

Aquest penúltim punt és un extra apreciable: com que el guardià és un component de ruta normal, pot pintar el marc compartit de l'àrea privada a més de comprovar la sessió.

function RutaProtegida() {
  const { usuari } = useUsuari();
  const location = useLocation();

  if (!usuari) {
    return <Navigate to="/acceso" replace state={{ tornarA: location }} />;
  }

  return (
    <div className={estils.areaPrivada}>
      <p className={estils.identificat}>
        Sessió iniciada com a <strong>{usuari.nom}</strong> ({usuari.rol})
      </p>
      <Outlet />
    </div>
  );
}

I l'arbre de rutes resultant, amb la branca protegida assenyalada:

flowchart TD
    RAIZ["/ · Disseny"]
    RAIZ --> IDX["index · PaginaCataleg"]
    RAIZ --> BICI["bicicletas/:bicicletaId"]
    RAIZ --> EST["estaciones"]
    EST --> ESTI["index · PaginaEstacions"]
    EST --> DET["' :estacionId ' · PaginaDetallEstacio"]
    DET --> FLO["index · PestanyaFlota"]
    DET --> INC["incidencias · PestanyaIncidencies"]
    RAIZ --> RES["reservas"]
    RES --> RESI["index · PaginaReserves"]
    RES --> NUE["nueva · PaginaNovaReserva"]
    RAIZ --> ACC["acceso · PaginaAcces"]
    RAIZ --> PROT["🔒 (sense path) RutaProtegida"]
    PROT --> ROL["🔒 (sense path) RequereixRol operario"]
    ROL --> TAL["taller · PaginaTaller"]
    RAIZ --> NF["* · PaginaNoTrobada"]
    style PROT fill:#fde68a
    style ROL fill:#fed7aa
    style TAL fill:#fecaca

  1. Autorització per rol: RequereixRol

Tenir sessió i tenir permís són coses diferents. Ana Ribera està identificada i tot i així no ha d'entrar a /taller. Convé separar els dos conceptes en dos components:

Concepte Pregunta Component Si falla
Autenticació Qui ets? RutaProtegida Redirigeix a /acceso
Autorització Pots fer això? RequereixRol Mostra «sense permisos» (403)
// src/components/RequereixRol.jsx
import { Outlet, Navigate, useLocation } from 'react-router';
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import PaginaSensePermisos from '../pagines/PaginaSensePermisos.jsx';

/**
 * Guardià d'autorització per rol.
 *
 * ⚠️ Només experiència d'usuari: l'autorització real és del servidor.
 *
 * Props:
 *  - rolsPermesos (array de cadenes, obligatori): p. ex. ['operario']
 *  - children (contingut, opcional): si falta, s'usa <Outlet />
 */
function RequereixRol({ rolsPermesos, children }) {
  const { usuari } = useUsuari();
  const location = useLocation();

  // Sense sessió: és un problema d'autenticació, no de permisos
  if (!usuari) {
    return <Navigate to="/acceso" replace state={{ tornarA: location }} />;
  }

  // Amb sessió però sense el rol adequat: 403, i la URL es conserva
  if (!rolsPermesos.includes(usuari.rol)) {
    return <PaginaSensePermisos rolsPermesos={rolsPermesos} />;
  }

  return children ?? <Outlet />;
}

export default RequereixRol;

El children ?? <Outlet /> del final permet usar el mateix component en els dos patrons: embolcallant fills o com a ruta de disseny. És una petita comoditat que evita mantenir dos components gairebé idèntics.

Al mapa, els dos guardians s'imbriquen i cadascun fa la seva feina:

{
  element: <RutaProtegida />,                          // hi ha sessió?
  children: [
    {
      element: <RequereixRol rolsPermesos={['operario']} />,   // és operari?
      children: [
        { path: 'taller', element: <PaginaTaller />, handle: { molla: 'Taller' } }
      ]
    }
  ]
}

Per què dos nivells i no un de sol? Perquè separen responsabilitats i perquè es poden reutilitzar per separat: demà /reservas pot necessitar sessió però qualsevol rol, i /informes pot admetre operario i supervisor. Cada guardià fa una cosa i es combinen segons calgui. Estrictament, RequereixRol ja cobreix el cas sense sessió, així que el podries usar sol; la imbricació expressa millor la intenció i fa evident al mapa que hi ha dues comprovacions diferents.

  1. 403 i 404: per què són pantalles diferents

És temptador reutilitzar PaginaNoTrobada quan algú no té permisos, i és un error. Els dos casos comuniquen coses diferents a l'usuari:

404 «No trobat» 403 «Sense permisos»
Què significa Aquesta adreça no existeix Existeix, però tu no la pots veure
Culpa de Un error tipogràfic o un enllaç trencat El perfil amb què has entrat
Què pot fer l'usuari Corregir la URL, anar a l'inici Entrar amb un altre compte, o demanar permís
Acció suggerida «Tornar al catàleg» «Canviar de compte» / «Sol·licitar accés»
Confusió si es barregen L'usuari creu que la pantalla no existeix i deixa d'intentar-ho
// src/pagines/PaginaSensePermisos.jsx
import { Link, useLocation } from 'react-router';
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import Avis from '../components/Avis.jsx';

/**
 * Pantalla 403 de CicloUrbano.
 * Props:
 *  - rolsPermesos (array de cadenes, opcional): per explicar què cal
 */
function PaginaSensePermisos({ rolsPermesos = [] }) {
  const { usuari, tancarSessio } = useUsuari();
  const location = useLocation();

  return (
    <section>
      <Avis to="advertencia" titol="No tens permisos per veure aquesta pantalla">
        <p>
          Has entrat com a <strong>{usuari?.nom}</strong> amb el rol{' '}
          <strong>{usuari?.rol}</strong>. La pantalla{' '}
          <code>{location.pathname}</code> és només per a{' '}
          {rolsPermesos.join(' o ') || 'altres perfils'}.
        </p>
        <p>
          Si creus que hauries de tenir accés, parla amb la persona responsable de
          la flota de CicloUrbano.
        </p>
      </Avis>

      <p>
        <Link to="/">Torna al catàleg</Link> ·{' '}
        <button type="button" onClick={tancarSessio}>
          Entra amb un altre compte
        </button>
      </p>
    </section>
  );
}

export default PaginaSensePermisos;

Es renderitza al seu lloc, no es redirigeix. Conservar la URL /taller té dos avantatges: l'usuari veu quina pantalla ha intentat obrir i pot corregir el problema (canviar de compte) sense tornar a buscar l'enllaç; i quan reporti la incidència, l'adreça és mig informe.

Un matís que es veu en aplicacions amb requisits de confidencialitat estrictes: de vegades es retorna 404 en lloc de 403 a propòsit, per no revelar que la pantalla existeix. És una decisió legítima, però és una decisió de producte i de seguretat que cal prendre conscientment i amb l'equip, no un descuit.

  1. L'estat intermedi: carregantSessio

Aquí hi ha la fallada més visible d'una protecció mal feta, i apareix tan bon punt la sessió es recupera d'algun lloc: durant el primeríssim render, usuari encara és null perquè encara no s'ha llegit res. El guardià ho interpreta com «no hi ha sessió» i redirigeix a /acceso algú que sí que la tenia. L'usuari veu una espurna del formulari d'accés i després, si hi ha sort, un salt de tornada.

La solució és representar el tercer estat del diagrama de l'apartat 1: «encara no ho sé».

// src/contextos/ContextUsuari.jsx — versió amb estat de càrrega
import { createContext, useContext, useState, useEffect } from 'react';
import { usuaris } from '../dades/domini.js';

const ContextUsuari = createContext(null);

const CLAU_SESSIO = 'ciclourbano:sesion';

export function ProveidorUsuari({ children }) {
  const [usuari, setUsuari] = useState(null);
  const [carregantSessio, setCarregantSessio] = useState(true);   // ← tercer estat

  useEffect(() => {
    // Recuperació de la sessió en arrencar. En una aplicació real,
    // aquí es validaria el token contra el servidor: seria asíncron.
    try {
      const desat = localStorage.getItem(CLAU_SESSIO);
      if (desat) {
        const id = JSON.parse(desat);
        setUsuari(usuaris.find((u) => u.id === id) ?? null);
      }
    } catch {
      // Emmagatzematge bloquejat o dades corruptes: s'arrenca sense sessió
    } finally {
      setCarregantSessio(false);   // passi el que passi, la comprovació ha acabat
    }
  }, []);

  function iniciarSessio(id) {
    const trobat = usuaris.find((u) => u.id === id) ?? null;
    setUsuari(trobat);
    try {
      if (trobat) localStorage.setItem(CLAU_SESSIO, JSON.stringify(trobat.id));
    } catch { /* sense espai o sense permisos: la sessió viu només en memòria */ }
  }

  function tancarSessio() {
    setUsuari(null);
    try {
      localStorage.removeItem(CLAU_SESSIO);
    } catch { /* ignorat expressament */ }
  }

  const valor = {
    usuari,
    esOperari: usuari?.rol === 'operario',
    carregantSessio,
    iniciarSessio,
    tancarSessio
  };

  return <ContextUsuari value={valor}>{children}</ContextUsuari>;
}

export function useUsuari() {
  const context = useContext(ContextUsuari);
  if (context === null) {
    throw new Error('useUsuari s\'ha d\'utilitzar dins de <ProveidorUsuari>');
  }
  return context;
}

I els guardians ho respecten abans de decidir res:

function RutaProtegida() {
  const { usuari, carregantSessio } = useUsuari();
  const location = useLocation();

  // 1. Encara no se sap: ni redirigir ni mostrar contingut protegit
  if (carregantSessio) {
    return <IndicadorDeCarrega missatge="Comprovant la teva sessió…" />;
  }

  // 2. Ja se sap i no hi ha sessió
  if (!usuari) {
    return <Navigate to="/acceso" replace state={{ tornarA: location }} />;
  }

  // 3. Ja se sap i hi ha sessió
  return <Outlet />;
}
// src/components/IndicadorDeCarrega.jsx
import estils from './IndicadorDeCarrega.module.css';

/**
 * Indicador d'espera accessible.
 * Props:
 *  - missatge (cadena, opcional, per defecte 'Carregant…')
 */
function IndicadorDeCarrega({ missatge = 'Carregant…' }) {
  return (
    <p className={estils.indicador} role="status" aria-live="polite">
      <span className={estils.roda} aria-hidden="true" />
      {missatge}
    </p>
  );
}

export default IndicadorDeCarrega;

El role="status" amb aria-live="polite" fa que un lector de pantalla anunciï el missatge sense interrompre el que estigui llegint, tal com es va establir a 03-06. La roda animada porta aria-hidden perquè és purament decorativa.

Tres regles de l'estat intermedi:

  1. Mentre carregantSessio sigui true, no es decideix res. Ni redirigir ni mostrar contingut protegit.
  2. PaginaAcces també ho ha de respectar. Si no, el formulari parpelleja just abans que la sessió es recuperi i redirigeixi.
  3. Compte amb l'espera artificial. Si la comprovació és instantània, un indicador que apareix i desapareix en 30 mil·lisegons molesta més que ajuda: aplica el retard de 200 ms de la BarraDeProgres de 06-04.

  1. Persistir la sessió amb useMagatzemLocal

L'apartat anterior va escriure l'accés a localStorage a mà, perquè es veiés la mecànica juntament amb el finally. Però el projecte ja té el hook de 05-06, i usar-lo simplifica bastant:

// src/contextos/ContextUsuari.jsx — amb useMagatzemLocal
import { useMagatzemLocal } from '../hooks/useMagatzemLocal.js';

export function ProveidorUsuari({ children }) {
  // El hook llegeix de forma mandrosa en el primer render: no hi ha estat intermedi
  const [idDesat, setIdDesat] = useMagatzemLocal('ciclourbano:sesion', null);

  const usuari = idDesat ? usuaris.find((u) => u.id === idDesat) ?? null : null;

  const valor = {
    usuari,
    esOperari: usuari?.rol === 'operario',
    carregantSessio: false,        // lectura síncrona: mai hi ha incertesa
    iniciarSessio: (id) => setIdDesat(id),
    tancarSessio: () => setIdDesat(null)
  };

  return <ContextUsuari value={valor}>{children}</ContextUsuari>;
}

Fixa't que usuari és un valor derivat de l'identificador desat, no un segon estat: exactament la distinció de 02-04 i 05-01. Desar l'objecte sencer a més de l'identificador crearia dues fonts de veritat que es desincronitzarien.

I que carregantSessio és false perquè useMagatzemLocal llegeix de forma síncrona a l'inicialitzador mandrós de useState. Aquest és el matís que decideix si necessites l'estat intermedi:

Origen de la sessió És síncron? Cal carregantSessio?
localStorage amb useMagatzemLocal No
Cookie llegida amb JavaScript No
Validació del token contra el servidor No
Biblioteca d'identitat externa No
Renovació silenciosa de token No

Tan bon punt la sessió de CicloUrbano es validi contra una API —el normal en producció—, l'estat intermedi torna a ser imprescindible. Per això valia la pena veure'l.

  1. Tokens, cookies i el límit de l'emmagatzematge del navegador

Aquí toca ser explícit sobre el que s'està desant i el que mai s'ha de desar.

El que hem desat: l'identificador 'usr-02'. És una preferència d'interfície, com el tema fosc: si un usuari l'edita a mà a localStorage, l'únic que aconsegueix és que la seva pròpia interfície mostri botons que el servidor rebutjarà tan bon punt els usi. No hi ha res a robar.

El que mai s'ha de desar allà: contrasenyes, claus d'API, dades personals de tercers i, amb matisos importants, tokens de sessió.

El problema de localStorage amb un token és concret: qualsevol JavaScript que s'executi a la teva pàgina el pot llegir. Una vulnerabilitat de tipus XSS —una entrada d'usuari que es pinta sense sanejar, una dependència compromesa— converteix el robatori del token en una línia de codi, i amb aquest token l'atacant actua com l'usuari des de qualsevol lloc.

L'alternativa habitual són les cookies httpOnly:

localStorage Cookie httpOnly + Secure + SameSite
Llegible per JavaScript No: el navegador l'envia, el codi no la veu
S'envia automàticament No: cal posar-la a cada capçalera Sí, a cada petició al domini
Exposada a XSS Molt menys
Exposada a CSRF No Sí, requereix SameSite i token anti-CSRF
Requereix col·laboració del servidor No : només el servidor la pot posar
Funciona entre dominis diferents Sí, manualment Requereix configuració acurada

I aquí convé ser honestos sobre l'abast d'aquest curs: la tria entre una i l'altra, la durada dels tokens, la renovació silenciosa, la revocació i la protecció contra CSRF no són decisions d'un desenvolupador d'interfícies treballant sol. Són decisions d'arquitectura que es prenen amb l'equip responsable de la seguretat i del servidor, perquè la meitat de la solució viu allà: una cookie httpOnly només la pot establir el servidor. Si en un projecte real et trobes decidint això pel teu compte al client, el senyal correcte és plantejar-ho a l'equip, no triar l'opció que sembli més còmoda.

El que sí que és responsabilitat teva al client, i no és poc:

  • Mai escriure secrets al codi. Tot el que hi ha al paquet de JavaScript és públic, incloses les variables d'entorn de Vite que comencen per VITE_.
  • No posar dades sensibles a la URL, que es desa a l'historial, es comparteix i apareix als registres del servidor.
  • No fiar-se mai d'una dada del client per decidir un permís al servidor.
  • Esborrar la sessió en tancar-la de veritat, i avisar el servidor perquè la invalidi.
  • Sanejar tot el que es pinti com a HTML. React escapa el contingut per defecte —ho vas veure a 01-04—, i dangerouslySetInnerHTML es diu així per alguna cosa.

  1. Ocultar a la interfície el que no es pot usar

Protegir la ruta evita que es vegi la pantalla; ocultar l'enllaç evita que l'usuari hi arribi per trobar-se un rebuig. Les dues coses es fan juntes, i són complementàries, no alternatives.

// src/components/MenuUsuari.jsx — sessió i rol reflectits al menú
import { NavLink, Link, useNavigate } from 'react-router';
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import { useTema } from '../contextos/ContextTema.jsx';
import BotoTema from './BotoTema.jsx';
import estils from './MenuUsuari.module.css';

function MenuUsuari() {
  const { usuari, esOperari, tancarSessio } = useUsuari();
  const navegar = useNavigate();

  function gestionarTancament() {
    tancarSessio();
    // En tancar sessió, fora de qualsevol pantalla protegida
    navegar('/', { replace: true });
  }

  if (!usuari) {
    return (
      <div className={estils.menu}>
        <BotoTema />
        <Link to="/acceso" className={estils.acces}>
          Inicia sessió
        </Link>
      </div>
    );
  }

  return (
    <div className={estils.menu}>
      <BotoTema />
      <span className={estils.nom}>{usuari.nom}</span>

      {/* L'enllaç al taller només existeix per a l'operari */}
      {esOperari && (
        <NavLink to="/taller" className={estils.enllac}>
          Panell de taller
        </NavLink>
      )}

      <button type="button" onClick={gestionarTancament}>
        Tanca sessió
      </button>
    </div>
  );
}

export default MenuUsuari;

El mateix criteri s'aplica dins de les pantalles: TargetaBicicleta ja llegeix esOperari del context des de 05-04 per mostrar el botó «Enviar al taller» només a qui el pot usar.

I el detall del tancament de sessió que s'oblida sovint: si l'usuari tanca sessió estant a /taller, cal treure'l d'allà. Sense aquest navegar('/', { replace: true }), el guardià ho detectaria i redirigiria a /acceso, cosa que funciona però és desconcertant: l'usuari ha premut «Tanca sessió» i acaba en una pantalla que li demana que entri. Portar-lo al catàleg és el que espera.

Ocultar davant de deshabilitar, perquè és una decisió de disseny recurrent:

Enfocament Quan Exemple
Ocultar L'opció mai estarà disponible per a aquest perfil «Panell de taller» per a un client
Deshabilitar amb explicació Podria estar disponible si alguna cosa canviés «Reservar» en una bicicleta en manteniment
Mostrar i explicar en prémer Es vol que es conegui la funcionalitat Una opció d'un pla superior

Ocultar-ho tot indiscriminadament té un cost: un usuari que no veu una opció no pot saber que existeix ni demanar-hi accés. Per a funcions per rol, ocultar és el normal; per a estats temporals, deshabilitar amb una explicació és millor.

  1. El mapa de rutes definitiu

Així queda src/rutes.jsx en acabar el mòdul, reunint tot el que s'ha construït a les cinc lliçons:

// src/rutes.jsx — versió definitiva del mòdul 6
import { createBrowserRouter } from 'react-router';

import Disseny from './components/Disseny.jsx';
import RutaProtegida from './components/RutaProtegida.jsx';
import RequereixRol from './components/RequereixRol.jsx';

import PaginaCataleg from './pagines/PaginaCataleg.jsx';
import PaginaFitxaBicicleta from './pagines/PaginaFitxaBicicleta.jsx';
import PaginaEstacions from './pagines/PaginaEstacions.jsx';
import PaginaDetallEstacio from './pagines/PaginaDetallEstacio.jsx';
import PestanyaFlota from './pagines/PestanyaFlota.jsx';
import PestanyaIncidencies from './pagines/PestanyaIncidencies.jsx';
import MarcReserves from './components/MarcReserves.jsx';
import PaginaReserves from './pagines/PaginaReserves.jsx';
import PaginaNovaReserva from './pagines/PaginaNovaReserva.jsx';
import PaginaAcces from './pagines/PaginaAcces.jsx';
import PaginaTaller from './pagines/PaginaTaller.jsx';
import PaginaNoTrobada from './pagines/PaginaNoTrobada.jsx';
import PaginaErrorRuta from './pagines/PaginaErrorRuta.jsx';

import { estacions } from './dades/domini.js';

export const router = createBrowserRouter([
  {
    path: '/',
    element: <Disseny />,
    errorElement: <PaginaErrorRuta />,
    handle: { molla: 'Inici' },
    children: [
      { index: true, element: <PaginaCataleg />, handle: { molla: 'Catàleg' } },

      {
        path: 'bicicletas/:bicicletaId',
        element: <PaginaFitxaBicicleta />,
        handle: { molla: 'Fitxa de bicicleta' }
      },

      {
        path: 'estaciones',
        handle: { molla: 'Estacions' },
        children: [
          { index: true, element: <PaginaEstacions /> },
          {
            path: ':estacionId',
            element: <PaginaDetallEstacio />,
            handle: {
              molla: (params) =>
                estacions.find((est) => est.id === params.estacionId)?.nom ?? 'Estació'
            },
            children: [
              { index: true, element: <PestanyaFlota /> },
              { path: 'incidencias', element: <PestanyaIncidencies /> }
            ]
          }
        ]
      },

      {
        path: 'reservas',
        element: <MarcReserves />,
        handle: { molla: 'Reserves' },
        children: [
          { index: true, element: <PaginaReserves /> },
          { path: 'nueva', element: <PaginaNovaReserva />, handle: { molla: 'Nova reserva' } }
        ]
      },

      { path: 'acceso', element: <PaginaAcces />, handle: { molla: 'Accés' } },

      // Branca protegida: sessió requerida, sense afegir segment a la URL
      {
        element: <RutaProtegida />,
        children: [
          {
            element: <RequereixRol rolsPermesos={['operario']} />,
            children: [
              { path: 'taller', element: <PaginaTaller />, handle: { molla: 'Taller' } }
            ]
          }
        ]
      },

      { path: '*', element: <PaginaNoTrobada /> }
    ]
  }
]);

Comprova el resultat amb aquesta taula d'escenaris:

Qui URL Què veu
Sense sessió / Catàleg, amb «Inicia sessió» al menú
Sense sessió /taller Redirigit a /acceso, amb avís i tornada posterior
Ana (usr-01, client) / Catàleg, sense enllaç al taller
Ana /taller Pantalla 403 «Sense permisos», URL conservada
Marc (usr-02, operari) /taller Panell del taller ✅
Marc /estaciones/est-02/incidencias Pestanya d'incidències, amb els botons d'operari
Qualsevol /estacionez Pantalla 404 «No trobat»

Errors Comuns i Consells

Creure que això és seguretat. L'error de fons de tota la lliçó. Un guardià de rutes és experiència d'usuari. Si el servidor no comprova els permisos a cada petició, no hi ha protecció de cap mena.

Redirigir durant l'estat de càrrega. Si el guardià decideix abans de saber si hi ha sessió, expulsa usuaris legítims. Comprova carregantSessio abans que res.

Oblidar replace en redirigir a /acceso. L'historial queda /taller → /acceso i «enrere» produeix un rebot infinit entre totes dues.

Perdre la destinació original. Sense state={{ tornarA: location }}, qui obre un enllaç protegit acaba al catàleg després d'identificar-se i ha de tornar a buscar l'enllaç.

Usar la pantalla de 404 per als casos de 403. L'usuari creu que la pantalla no existeix i no se li acut entrar amb un altre compte.

Redirigir en lloc de mostrar el 403. En perdre la URL, l'usuari no sap què intentava obrir ni ho pot corregir.

Ocultar l'enllaç i no protegir la ruta. Escriure l'adreça a mà n'hi ha prou per saltar-se-ho. Es fan les dues coses.

Protegir la ruta i no protegir l'API. És el mateix que no protegir res, però amb la falsa sensació d'haver-ho fet.

Desar tokens a localStorage sense pensar-hi. Qualsevol XSS els exposa. És una decisió d'arquitectura per a l'equip, no un detall d'implementació del client.

No treure l'usuari d'una pantalla protegida en tancar sessió. Acaba al formulari d'accés just després de prémer «Tanca sessió»: desconcertant.

Consell: declara els permisos al mapa de rutes. Amb handle (06-03), el mapa esdevé l'única font de veritat i es pot recórrer per generar el menú automàticament:

{ path: 'taller', element: <PaginaTaller />, handle: { molla: 'Taller', rols: ['operario'] } }

Consell: prova sempre els quatre casos. Sense sessió, amb sessió i rol insuficient, amb sessió i rol correcte, i recarregant (F5) en cadascun. La recàrrega és la que descobreix els problemes de l'estat intermedi.

Consell: escriu l'advertència al propi codi. Un comentari a la capçalera de RutaProtegida que recordi que l'autorització real és al servidor evita que d'aquí a un any algú —potser tu— doni per fet que aquest component protegeix alguna cosa.

Exercicis

Exercici 1: reservar exigeix sessió

Un visitant sense sessió pot avui obrir /reservas/nueva i emplenar el formulari. Protegeix aquesta pantalla, però no /reservas, que ha de continuar sent visible per mostrar l'estat buit amb una invitació a entrar. Requisits:

  • /reservas/nueva requereix sessió de qualsevol rol.
  • Després d'identificar-se, l'usuari torna a /reservas/nueva.
  • MarcReserves continua funcionant en totes dues.

Indica quin dels dos patrons faries servir i per què.

Exercici 2: menú generat des del mapa

Afegeix rols al handle de les rutes que ho necessitin i escriu un component MenuPrincipal que generi els enllaços de Capcalera recorrent el mapa de rutes, mostrant només els que l'usuari actual pot visitar. Explica quin avantatge té això sobre la llista escrita a mà.

Exercici 3: trobar les fallades del guardià

Aquest guardià té quatre problemes. Identifica'ls i corregeix-lo.

function RutaProtegida({ children }) {
  const { usuari } = useUsuari();
  const navegar = useNavigate();

  useEffect(() => {
    if (!usuari) {
      navegar('/acceso');
    }
  }, []);

  return children;
}

Solucions

Solució 1

El patró 2 (ruta de disseny protegida), encara que només hi hagi una pantalla, per dues raons: manté MarcReserves com a pare comú de totes dues filles sense duplicar-lo, i deixa preparada la branca per quan hi hagi més pantalles de reserves que requereixin sessió (editar, cancel·lar). Amb el patró 1 caldria embolcallar l'element de cada filla i seria fàcil oblidar-se'n a la següent.

{
  path: 'reservas',
  element: <MarcReserves />,
  handle: { molla: 'Reserves' },
  children: [
    // Pública: l'estat buit convida a entrar
    { index: true, element: <PaginaReserves /> },

    // Protegida: branca sense path dins del marc
    {
      element: <RutaProtegida />,
      children: [
        { path: 'nueva', element: <PaginaNovaReserva />, handle: { molla: 'Nova reserva' } }
      ]
    }
  ]
}

I PaginaReserves distingeix els dos casos de l'estat buit:

function PaginaReserves() {
  const { estat } = useReserves();
  const { usuari } = useUsuari();

  if (estat.reserves.length === 0) {
    return usuari ? (
      <p>
        Encara no tens reserves. <Link to="/reservas/nueva">Crea'n una</Link>
      </p>
    ) : (
      <p>
        <Link to="/acceso">Inicia sessió</Link> per veure les teves reserves i crear-ne una de nova.
      </p>
    );
  }

  return <PanellReserves reserves={estat.reserves} />;
}

La tornada a la destinació original funciona sense escriure res més: RutaProtegida desa state={{ tornarA: location }} i PaginaAcces la consumeix.

Solució 2

// src/rutes.jsx — s'afegeix rols al handle on calgui
{ index: true, element: <PaginaCataleg />, handle: { molla: 'Catàleg', alMenu: true } }
{ path: 'estaciones', handle: { molla: 'Estacions', alMenu: true }, children: [ /* … */ ] }
{ path: 'reservas', handle: { molla: 'Reserves', alMenu: true, requereixSessio: true }, /* … */ }
{ path: 'taller', element: <PaginaTaller />, handle: { molla: 'Taller', alMenu: true, rols: ['operario'] } }
// src/components/MenuPrincipal.jsx
import { NavLink } from 'react-router';
import { useUsuari } from '../contextos/ContextUsuari.jsx';
import { router } from '../rutes.jsx';
import estils from './Capcalera.module.css';

/**
 * Recorre el mapa de rutes i retorna les entrades de menú visibles.
 */
function recollirEntrades(rutes, prefix = '') {
  return rutes.flatMap((ruta) => {
    const cami = ruta.index
      ? prefix || '/'
      : [prefix, ruta.path].filter(Boolean).join('/').replace('//', '/');

    const propia = ruta.handle?.alMenu
      ? [{ to: cami.startsWith('/') ? cami : `/${cami}`, handle: ruta.handle, index: Boolean(ruta.index) }]
      : [];

    const filles = ruta.children ? recollirEntrades(ruta.children, ruta.path ? cami : prefix) : [];

    return [...propia, ...filles];
  });
}

function MenuPrincipal() {
  const { usuari } = useUsuari();

  const entrades = recollirEntrades(router.routes).filter(({ handle }) => {
    if (handle.requereixSessio && !usuari) return false;
    if (handle.rols && !handle.rols.includes(usuari?.rol)) return false;
    return true;
  });

  return (
    <nav aria-label="Navegació principal">
      {entrades.map((entrada) => (
        <NavLink
          key={entrada.to}
          to={entrada.to}
          end={entrada.index}
          className={({ isActive }) =>
            isActive ? `${estils.enllac} ${estils.actiu}` : estils.enllac
          }
        >
          {typeof entrada.handle.molla === 'function' ? entrada.handle.molla({}) : entrada.handle.molla}
        </NavLink>
      ))}
    </nav>
  );
}

export default MenuPrincipal;

Els avantatges sobre la llista escrita a mà: el mapa de rutes passa a ser l'única font de veritat, així que afegir una pantalla al menú és afegir alMenu: true a la seva ruta —impossible que el menú i les rutes es desincronitzin—; les regles de visibilitat es declaren al costat de la ruta que protegeixen, en lloc de repetir-se a la capçalera; i si demà canvia un path, l'enllaç s'actualitza sol.

El cost, que convé reconèixer: el recorregut del mapa per construir els camins és delicat amb rutes índex, rutes sense path i rutes imbricades —d'aquí l'embolic de recollirEntrades—, i per a un menú de tres entrades pot ser més codi del que estalvia. En una aplicació amb vint pantalles i diversos rols, compensa amb escreix. És el mateix judici de sempre: l'abstracció es paga, i cal saber quan es recupera la inversió.

Solució 3

Els quatre problemes:

  1. return children s'executa sempre, fins i tot sense sessió. L'efecte s'executa després del render, així que el contingut protegit es pinta durant un instant abans de la redirecció: exactament el que es volia evitar. És la fallada més greu.
  2. Array de dependències buit. Si l'usuari tanca sessió estant a dins, l'efecte no es torna a executar i el guardià deixa de protegir-lo. Ha d'incloure usuari i navegar.
  3. Falta replace. L'historial queda /taller → /acceso i «enrere» rebota indefinidament.
  4. No es desa la destinació original. Després d'identificar-se, l'usuari acaba on el porti el formulari, no on volia anar.

I un cinquè, latent tan bon punt la sessió es recuperi de forma asíncrona: no es comprova carregantSessio, així que expulsaria usuaris amb sessió vàlida durant el primer render.

La versió corregida és la de l'apartat 8:

function RutaProtegida({ children }) {
  const { usuari, carregantSessio } = useUsuari();
  const location = useLocation();

  if (carregantSessio) {
    return <IndicadorDeCarrega missatge="Comprovant la teva sessió…" />;
  }

  if (!usuari) {
    return <Navigate to="/acceso" replace state={{ tornarA: location }} />;
  }

  return children ?? <Outlet />;
}

La lliçó de fons: decidir al render amb <Navigate /> en lloc de en un efecte resol de cop els problemes 1, 2 i bona part del 3, perquè no hi ha cap moment en què el contingut protegit arribi a existir. És l'aplicació directa de la regla de 06-04.

Conclusió

Comencem i acabem pel mateix, perquè és l'única cosa d'aquesta lliçó que no admet matisos: tot el control d'accés que has escrit és experiència d'usuari, no seguretat. El codi es descarrega sencer al navegador de l'usuari, i allà ell pot canviar variables, saltar-se punts d'interrupció, editar localStorage i trucar directament a la teva API sense passar per la interfície. Un guardià de rutes evita que un usuari legítim vegi pantalles que no li serveixen i fa que l'aplicació sigui coherent; l'autorització real es comprova sempre al servidor, a cada petició, amb les credencials de qui la fa. Si el servidor retorna les dades del taller a qualsevol que les demani, RutaProtegida no protegeix res.

Dit això, has construït un control d'accés complet i ben fet. Reutilitzant el ContextUsuari de 05-04 —usuari, esOperari, iniciarSessio, tancarSessio— i amb un accés fictici a /acceso que tria entre Ana Ribera (usr-01, client) i Marc Solé (usr-02, operari), tens dos patrons a la teva disposició: el component guardià que embolcalla el que protegeix i redirigeix amb <Navigate to="/acceso" replace state={{ tornarA: location }} />, i la ruta de disseny protegida —una ruta sense path l'element de la qual és el guardià i que agrupa diverses filles amb <Outlet />—, preferible tan bon punt hi ha més d'una pantalla perquè es declara una vegada, és impossible oblidar-la en afegir-ne la següent i permet pintar el marc comú de l'àrea privada. Per sobre d'ells, RequereixRol separa l'autenticació («qui ets?») de l'autorització («pots?»), amb una pantalla 403 que conserva la URL, explica amb quin rol has entrat i ofereix canviar de compte, clarament diferent del 404 que diu que l'adreça no existeix: barrejar-les deixa l'usuari creient que la pantalla no hi és. Has resolt l'estat intermedi amb carregantSessio i un indicador accessible, perquè ningú sigui expulsat mentre encara es comprova si té sessió —imprescindible tan bon punt la validació sigui asíncrona—, has persistit la sessió amb el useMagatzemLocal de 05-06 desant només l'identificador i derivant l'usuari, i saps que la tria entre localStorage i cookies httpOnly per a un token és una decisió d'arquitectura que es planteja a l'equip de seguretat, no una cosa que es decideixi només al client. I has completat el quadre ocultant al menú el que el rol no pot usar, a més de protegir la ruta: les dues coses, mai una de sola.

Amb això es tanca el Mòdul 6. CicloUrbano ha passat d'una única pantalla a una aplicació enrutada de veritat: nou rutes amb Disseny com a arrel persistent, segments dinàmics per a bicicletes i estacions, filtres a la URL, pestanyes imbricades, molles de pa generades des del propi mapa, contenció d'errors per branca, navegació programàtica després de confirmar una reserva, bloqueig de sortida amb canvis sense desar i una branca protegida per sessió i per rol.

I apareix el problema següent, que ja es nota en el codi que acabes d'escriure. L'estat està repartit per tota l'aplicació: la sessió a ContextUsuari, el tema a ContextTema, els avisos a ContextAvisos, les reserves en un reductor dins de ContextReserves, el filtre del catàleg a la URL i el terme de cerca en un useState de la pàgina. Cadascun viu en un lloc diferent per una raó diferent, alguns components consumeixen tres contextos alhora i ja no és clar on hauria d'anar la propera dada que aparegui ni com evitar que un canvi en un context repinti mitja aplicació. El Mòdul 7: Gestió de l'Estat posa ordre en tot això: quins tipus d'estat existeixen i on ha de viure cadascun, fins on arriba el context com a estratègia global i quins són els seus costos reals, què aporta Redux amb el seu magatzem únic, les seves accions i els seus reductors, com es connecta a React, i per què les dades que vénen d'un servidor són una categoria a part amb les seves pròpies eines. La propera lliçó és Introducció a la Gestió de l'Estat.

Curs de React

Mòdul 1: Introducció a React

Mòdul 2: Components de React

Mòdul 3: Treballar amb Esdeveniments

Mòdul 4: Conceptes Avançats de Components

Mòdul 5: Hooks de React

Mòdul 6: Enrutament a React

Mòdul 7: Gestió de l'Estat

Mòdul 8: Optimització del Rendiment

Mòdul 9: Proves a React

Mòdul 10: Temes Avançats

Mòdul 11: Projecte: Construir una Aplicació Completa

© Copyright 2026. Tots els drets reservats