La lliçó anterior va deixar l'aparador ciclourbano-web funcionant amb renderitzat dinàmic: cada visita provoca un render al servidor. Per al catàleg amb disponibilitat en viu això és correcte. Per a la pàgina de condicions del servei, que canvia dues vegades l'any, és un malbaratament difícil de justificar: el servidor genera exactament el mateix HTML milers de vegades al dia, consumeix CPU, afegeix latència i pot caure. Aquesta lliçó va a l'altre extrem de la taula del mòdul —generar l'HTML una sola vegada, en la construcció— i després busca el punt intermedi que resol la majoria dels casos reals: la regeneració incremental. Aprendràs a decidir quin contingut guanya sent estàtic, a fer que Next.js prerenderitzi rutes dinàmiques com la fitxa de cada bicicleta, a llegir la sortida de next build per saber què ha fet realment, a caducar i refrescar l'HTML per temps o sota demanda, i a tancar l'arquitectura híbrida de CicloUrbano: aparador estàtic i aplicació de gestió a la SPA.
Contingut
- El malbaratament de renderitzar mil vegades el mateix
- Què és exactament la generació estàtica
- Inventari de CicloUrbano: què guanya sent estàtic i què no
- Com decideix Next.js entre estàtic i dinàmic
- Llegir la sortida de
next build: ○, ● i ƒ generateStaticParams: prerenderitzar rutes dinàmiquesdynamicParams: què passa amb un identificador nou- Revalidació incremental (ISR)
- Obsolet mentre es revalida: la línia temporal
- Revalidació sota demanda:
revalidatePathirevalidateTag - Triar bé el temps de revalidació
- Contingut des de fitxers locals o des d'un CMS
- Rutes estàtiques especials:
sitemap,robotsi les imatges de previsualització - Imatges i tipografies:
next/imageinext/font - Quan no convé l'estàtic
- Taula resum definitiva i arquitectura híbrida
- El malbaratament de renderitzar mil vegades el mateix
Posa xifres a l'aparador de CicloUrbano. Suposa 50.000 visites diàries a la pàgina de condicions del servei, i que renderitzar-la costa 40 ms de CPU al servidor.
| Renderitzat dinàmic (SSR) | Generació estàtica (SSG) | |
|---|---|---|
| Renders al dia | 50.000 | 0 (un a la construcció) |
| CPU consumida | ~33 minuts diaris | ~40 ms, una vegada |
| TTFB típic | 80-250 ms | 10-30 ms des del CDN |
| Sobreviu si l'API cau? | No | Sí |
| Sobreviu a un pic de trànsit? | Només escalant | Sí, el CDN ho absorbeix |
| Cost d'allotjament | Servidor Node sempre encès | Fitxers en un CDN |
Totes les files apunten en la mateixa direcció i el motiu és un de sol: el resultat no depèn de la petició. Quan l'HTML és idèntic per a tothom i no canvia entre visites, generar-lo cada vegada és treball repetit.
Hi ha una diferència addicional que no apareix a la taula i que sol decidir l'arquitectura: la resiliència. Una pàgina estàtica ja està escrita al disc d'un CDN. Si json-server deixa de respondre, la fitxa de bici-002 es continua servint amb les últimes dades conegudes. Amb SSR, aquesta mateixa fitxa retorna un 500. Per a l'aparador públic d'una empresa, que la web es mantingui en peu quan el sistema intern falla no és un luxe: és el que s'espera.
- Què és exactament la generació estàtica
La generació estàtica consisteix a executar els components durant next build, desar l'HTML resultant al disc i servir aquest fitxer a tothom.
flowchart LR
subgraph CONSTRUCCIO["next build · una vegada"]
A["Components de servidor"] --> B["fetch a l'API"]
B --> C["React renderitza"]
C --> D["HTML + payload RSC<br/>guardats al disc"]
end
D --> E["CDN"]
E --> F["Visitant 1"]
E --> G["Visitant 2"]
E --> H["Visitant 50.000"]
Tres precisions que eviten malentesos habituals:
- Estàtic no significa «sense React». L'HTML prerenderitzat s'hidrata igual que el del SSR, i les illes de client (
SelectorTipus,BotoTema) continuen sent interactives. L'única cosa que canvia és quan es va generar aquest HTML. - Estàtic no significa «sense dades». Els
fetchs'executen; simplement s'executen a la màquina de construcció, no a cada petició. - Estàtic no significa «per sempre». Amb ISR, aquest HTML caduca i es regenera. És el que veurem a partir de l'apartat 8.
El codi d'una pàgina estàtica és idèntic al d'una dinàmica. No hi ha una API diferent: l'única cosa que canvia és si fas servir alguna cosa que obligui a esperar la petició.
// src/app/condiciones/page.jsx — pàgina estàtica sense fer res especial
export const metadata = {
title: 'Condicions del servei',
description: 'Condicions de lloguer de bicicletes de CicloUrbano.',
};
export default function PaginaCondicions() {
return (
<article>
<h1>Condicions del servei</h1>
<p>El lloguer es factura per hores completes des del moment del desbloqueig.</p>
<h2>Fiances</h2>
<p>Les bicicletes de càrrega requereixen una fiança de 30 € reemborsable.</p>
</article>
);
}Aquesta pàgina no llegeix cookies, no llegeix searchParams i no fa cap fetch sense memòria cau. Next.js la prerenderitza sense que li ho demanis.
- Inventari de CicloUrbano: què guanya sent estàtic i què no
El criteri es pot reduir a dues preguntes: el resultat depèn de qui demana la pàgina? i amb quina freqüència canvia?
| Pantalla | Depèn de l'usuari? | Freqüència de canvi | Estratègia | Per què |
|---|---|---|---|---|
/condiciones, /sobre-nosotros, /tarifas |
No | Dues vegades l'any | SSG pur | Text fix; regenerar-lo a cada visita no aporta res |
/estaciones (llistat) |
No | Alta o baixa segons què es mostri | ISR 5 min | Nom i barri són fixos; les places lliures canvien |
/bicicletas/[bicicletaId] (fitxa) |
No | Diverses vegades al dia | ISR 60 s | Centenars de pàgines, canvis moderats, molt valor de SEO |
/ (catàleg amb disponibilitat en viu) |
No, però ha de ser exacte | Constant | SSR | Mostrar disponibilitat obsoleta enganya l'usuari |
/reservas |
Sí | Constant | CSR (SPA) | Dades privades; res a prerenderitzar |
/taller |
Sí, a més per rol | Constant | CSR (SPA) | Darrere de RutaProtegida; cap valor de SEO |
Observa el matís de la fitxa de bicicleta enfront del catàleg. Tots dos mostren l'estat de la bicicleta, però el compromís és diferent: el catàleg promet disponibilitat ara mateix i per això no es pot permetre dades d'un minut abans; la fitxa és sobretot una pàgina de contingut —model, tipus, preu, estació—, i un desfasament de 60 segons a l'etiqueta d'estat és acceptable si a canvi se serveix des del CDN. La decisió no és tècnica, és de producte: quanta obsolescència tolera cada pantalla.
- Com decideix Next.js entre estàtic i dinàmic
La regla de l'App Router s'enuncia en una frase:
Tota ruta és estàtica per defecte. Es torna dinàmica en el moment que fa servir alguna cosa que només es coneix en el moment de la petició.
Aquests «alguna cosa» són exactament els detonants que ja vas veure a 10-01, ara en el seu paper complet:
| Detonant | Efecte | Com evitar-ho si vols estàtic |
|---|---|---|
fetch(..., { cache: 'no-store' }) |
Dinàmica | cache: 'force-cache' o next: { revalidate: N } |
Un fetch sense opcions (Next.js 15) |
Dinàmica | Igual que l'anterior: la memòria cau cal demanar-la |
await cookies() |
Dinàmica | Aïllar en un component de client o dins de <Suspense> |
await headers(), connection() |
Dinàmica | Igual |
La prop searchParams d'una page |
Dinàmica | Llegir el paràmetre en un component de client amb useSearchParams |
export const dynamic = 'force-dynamic' |
Dinàmica | Treure'l |
export const revalidate = 0 |
Dinàmica | Posar un valor més gran que zero |
I els dos interruptors explícits que convé conèixer:
// Força el comportament de tota la ruta. Valors possibles:
export const dynamic = 'auto'; // per defecte: Next.js decideix
export const dynamic = 'force-static'; // força estàtic (cookies() retorna buit)
export const dynamic = 'force-dynamic'; // força render a cada petició
export const dynamic = 'error'; // estàtic, i ERROR si alguna cosa el fa dinàmicdynamic = 'error' és una eina excel·lent en un projecte real: converteix «creia que aquesta pàgina era estàtica» en una fallada de construcció. És la mateixa filosofia de l'anàlisi estàtica del mòdul 9 aplicada al renderitzat.
Un avís important sobre searchParams: fer-lo servir en una page la fa dinàmica sencera, perquè el servidor necessita la URL completa. Si només vols reaccionar a ?tipo= sense perdre el prerenderitzat, la solució és llegir el paràmetre en un component de client i filtrar allà, o posar cada filtre en la seva pròpia ruta (/catalogo/electricas).
- Llegir la sortida de
next build: ○, ● i ƒ
next build: ○, ● i ƒNo cal suposar res: next build diu exactament què ha fet amb cada ruta.
Route (app) Size First Load JS Revalidate ┌ ○ / 1.2 kB 102 kB ├ ○ /_not-found 142 B 88 kB ├ ● /bicicletas/[bicicletaId] 0.9 kB 101 kB 1m ├ ├ /bicicletas/bici-001 ├ ├ /bicicletas/bici-002 ├ └ [+3 more paths] ├ ○ /condiciones 136 B 88 kB ├ ● /estaciones 310 B 89 kB 5m ├ ● /estaciones/[estacionId] 450 B 92 kB 5m ├ ƒ /catalogo 1.1 kB 102 kB └ ○ /sitemap.xml 136 B 88 kB ○ (Static) prerendered as static content ● (SSG) prerendered as static HTML (uses generateStaticParams) ƒ (Dynamic) server-rendered on demand
Com es llegeix aquesta taula:
| Símbol | Significat | Quan es genera l'HTML |
|---|---|---|
| ○ Static | Ruta sense paràmetres, prerenderitzada | A next build |
| ● SSG | Ruta dinàmica prerenderitzada amb generateStaticParams |
A next build, una pàgina per paràmetre |
| ƒ Dynamic | Renderitzada al servidor a cada petició | A cada visita |
I les columnes:
- Size: el JavaScript propi d'aquesta ruta.
- First Load JS: el total que descarrega qui entra per allà, inclòs el codi compartit. És la xifra del mòdul 8 i continua important.
- Revalidate: el temps de caducitat, si n'hi ha.
Les línies indentades sota ● són les pàgines concretes generades. Si esperaves una ○ i veus una ƒ, alguna cosa ha activat un detonant: revisa cookies(), searchParams i les opcions de fetch. Aquesta comanda hauria de formar part de la teva rutina abans de desplegar, igual que npm test.
generateStaticParams: prerenderitzar rutes dinàmiques
generateStaticParams: prerenderitzar rutes dinàmiquesUna ruta com /bicicletas/[bicicletaId] té un problema evident: en la construcció, Next.js no sap quins identificadors existeixen. Pot prerenderitzar /condiciones perquè és una sola pàgina, però no pot endevinar que hi ha una bici-001 i una bici-002.
generateStaticParams és la funció que li ho diu. S'executa durant la construcció, retorna la llista de valors possibles del segment dinàmic, i Next.js genera una pàgina per cadascun.
// src/app/bicicletas/[bicicletaId]/page.jsx
import { notFound } from 'next/navigation';
import EtiquetaEstat from '@/components/EtiquetaEstat';
const API = 'http://localhost:3001';
export async function generateStaticParams() {
const resposta = await fetch(`${API}/bicicletas`);
const bicicletes = await resposta.json();
// Cada objecte de l'array correspon a un [bicicletaId] del nom de carpeta.
return bicicletes.map((bicicleta) => ({ bicicletaId: bicicleta.id }));
}
async function obtenirBicicleta(bicicletaId) {
const resposta = await fetch(`${API}/bicicletas/${bicicletaId}`, {
next: { revalidate: 60, tags: [`bicicleta-${bicicletaId}`] },
});
return resposta.ok ? resposta.json() : null;
}
export async function generateMetadata({ params }) {
const { bicicletaId } = await params;
const bicicleta = await obtenirBicicleta(bicicletaId);
if (!bicicleta) return { title: 'Bicicleta no trobada' };
return {
title: `${bicicleta.model} · ${bicicleta.preuHora.toFixed(2)} €/h`,
description: `Bicicleta ${bicicleta.tipus} disponible per llogar per hores.`,
};
}
export default async function PaginaFitxaBicicleta({ params }) {
const { bicicletaId } = await params;
const bicicleta = await obtenirBicicleta(bicicletaId);
if (!bicicleta) notFound();
return (
<article>
<h1>{bicicleta.model}</h1>
<EtiquetaEstat estat={bicicleta.estat} />
<p>{bicicleta.preuHora.toFixed(2)} €/h · estació {bicicleta.estacioId}</p>
</article>
);
}Anàlisi línia a línia del que ha canviat respecte a 10-01:
- La forma del valor retornat importa. Cada objecte ha de tenir una clau amb el nom exacte del segment: la carpeta és
[bicicletaId], així que la clau ésbicicletaId. I el valor és sempre una cadena, encara que l'identificador fos numèric. cache: 'no-store'ha desaparegut i en el seu lloc hi hanext: { revalidate: 60 }. Això és el que permet que la ruta sigui estàtica (●al build) i alhora es refresqui cada minut.- L'etiqueta
tags: ['bicicleta-<id>']no fa res per si sola; és l'ancoratge per a la revalidació sota demanda de l'apartat 10. generateStaticParamsigenerateMetadatai el component comparteixen elsfetchgràcies a la deduplicació automàtica que ja has vist.
En rutes imbricades, cada nivell dinàmic aporta la seva funció. Per a /estaciones/[estacionId]/incidencias, el generateStaticParams de [estacionId] s'hereta i no cal repetir-lo:
// src/app/estaciones/[estacionId]/layout.jsx
export async function generateStaticParams() {
const resposta = await fetch('http://localhost:3001/estaciones');
const estacions = await resposta.json();
return estacions.map((estacio) => ({ estacionId: estacio.id }));
}Amb les tres estacions del cànon, la construcció genera est-01, est-02 i est-03, cadascuna amb la seva pestanya índex i la seva pestanya d'incidències: sis pàgines.
dynamicParams: què passa amb un identificador nou
dynamicParams: què passa amb un identificador nouPregunta inevitable: l'equip de CicloUrbano dona d'alta bici-006 després del desplegament. Aquesta pàgina no existeix al disc. Què passa quan algú la visita?
Ho decideix dynamicParams:
| Valor | Comportament davant un paràmetre no generat |
|---|---|
true (per defecte) |
Es renderitza al servidor la primera vegada, es desa en memòria cau i les visites següents es serveixen des d'ella |
false |
Retorna un 404 directament, sense cridar el component |
Quan fer servir cadascun:
trueper a catàlegs que creixen: bicicletes, estacions, articles. És el comportament desitjat a CicloUrbano: la bicicleta nova funciona a l'instant, només que la primera visita paga el render.falsequan el conjunt és tancat i conegut: idiomes suportats, categories fixes, pàgines legals. Així una URL inventada no dispara un render inútil, cosa que a més tanca una via d'abús.
Amb dynamicParams = true, generateStaticParams deixa de ser una llista exhaustiva i passa a ser una llista d'escalfament. Un patró molt útil en catàlegs grans és prerenderitzar només el més visitat i deixar que la resta es generi sota demanda:
export async function generateStaticParams() {
const resposta = await fetch('http://localhost:3001/bicicletas');
const bicicletes = await resposta.json();
// Només les disponibles: són les que la gent consulta i comparteix.
return bicicletes
.filter((bicicleta) => bicicleta.estat === 'disponible')
.map((bicicleta) => ({ bicicletaId: bicicleta.id }));
}Amb el cànon de CicloUrbano, això genera bici-001, bici-004 i bici-005 a la construcció; bici-002 (llogada) i bici-003 (en manteniment) es generaran la primera vegada que algú les demani.
- Revalidació incremental (ISR)
Aquí hi ha la idea que fa utilitzable la generació estàtica en un lloc amb dades reals, i és la quarta columna de la taula del mòdul.
ISR (Incremental Static Regeneration) serveix la pàgina estàtica desada, i quan aquesta pàgina porta més de N segons sense regenerar-se, la torna a generar en segon pla. El visitant que arriba just després de la caducitat no espera: rep la versió antiga, i la nova queda llesta per al següent.
Es declara de dues formes, que es poden combinar:
// A) Per ruta: afecta tota la pàgina.
export const revalidate = 300; // segons
// B) Per petició: cada fetch té el seu propi cicle de vida.
const resposta = await fetch(`${API}/bicicletas/${id}`, {
next: { revalidate: 60 },
});Diferències i quin fer servir:
export const revalidate = N |
next: { revalidate: N } |
|
|---|---|---|
| Abast | Tota la ruta | Una petició concreta |
| Granularitat | Baixa | Alta |
| Quan fer-lo servir | La pàgina té un ritme únic | Dades amb ritmes diferents a la mateixa pàgina |
Quan conviuen, guanya el més curt: si la ruta declara 300 i un fetch declara 60, la pàgina es regenera cada 60 segons.
Exemple real amb dos ritmes a la mateixa pàgina, el llistat d'estacions:
// src/app/estaciones/page.jsx
import TargetaEstacio from '@/components/TargetaEstacio';
const API = 'http://localhost:3001';
// Sostre de la pàgina: com a molt, 10 minuts d'obsolescència.
export const revalidate = 600;
export default async function PaginaEstacions() {
const [estacions, ocupacio] = await Promise.all([
// El nom i el barri d'una estació no canvien mai: una hora està bé.
fetch(`${API}/estaciones`, { next: { revalidate: 3600 } }).then((r) => r.json()),
// Les places lliures canvien constantment: un minut.
fetch(`${API}/ocupacion`, { next: { revalidate: 60 } }).then((r) => r.json()),
]);
return (
<section>
<h1>Les nostres estacions</h1>
<ul>
{estacions.map((estacio) => (
<li key={estacio.id}>
<TargetaEstacio
estacio={estacio}
placesLliures={ocupacio[estacio.id] ?? 0}
/>
</li>
))}
</ul>
</section>
);
}En el build, aquesta ruta apareix com ● amb Revalidate: 1m —el mínim efectiu—, i la pàgina se serveix des del CDN excepte la regeneració ocasional.
- Obsolet mentre es revalida: la línia temporal
Aquest és el mecanisme exacte, i val la pena entendre'l bé perquè explica una confusió molt habitual: «he canviat la dada, he recarregat i continuo veient la vella». No és una fallada; és el disseny.
Amb revalidate = 60 i una regeneració que triga 2 segons:
sequenceDiagram
participant V1 as Visitant A (t=0s)
participant V2 as Visitant B (t=75s)
participant V3 as Visitant C (t=78s)
participant N as Next.js
participant API as API
V1->>N: GET /bicicletas/bici-002
N-->>V1: HTML cachejat (fresc) · rapid
Note over N: Cache generada en t=0. Caduca en t=60.
V2->>N: GET /bicicletas/bici-002
N-->>V2: HTML cachejat (OBSOLET) · rapid
Note over N: Caducada: es llanca la regeneracio EN SEGON PLA
N->>API: GET /bicicletas/bici-002
API-->>N: JSON actualitzat
Note over N: Nova cache llesta en t=77
V3->>N: GET /bicicletas/bici-002
N-->>V3: HTML NOU · rapid
El que cal retenir:
- Ningú espera mai. Ni tan sols el visitant B, que és qui dispara la regeneració. Aquest patró s'anomena stale-while-revalidate.
- El visitant B veu contingut obsolet, i és una decisió conscient: es prefereix una resposta ràpida i lleugerament antiga a una resposta lenta i exacta.
revalidate: 60no significa «es regenera cada 60 segons». Significa «passats 60 segons, la pròxima visita dispara la regeneració». Si ningú entra en tres dies, no es regenera res. La memòria cau és mandrosa, no un cronòmetre.- La regeneració és única. Si arriben cent visitants alhora després de la caducitat, es dispara una sola regeneració; els cent reben la versió antiga.
D'aquí surt una conseqüència pràctica: en desenvolupament (npm run dev) no veuràs aquest comportament, perquè el mode de desenvolupament renderitza sempre. Per comprovar l'ISR de veritat cal executar npm run build && npm run start. És la mateixa disciplina que al mòdul 8 amb el Profiler: es mesura sobre el build de producció.
- Revalidació sota demanda:
revalidatePath i revalidateTag
revalidatePath i revalidateTagL'ISR per temps resol la majoria dels casos, però deixa un buit: quan l'equip de CicloUrbano dona d'alta bici-006 o baixa el preu de les elèctriques, no vol esperar 60 segons ni 10 minuts. Vol que el canvi es reflecteixi ja.
La revalidació sota demanda inverteix el control: en lloc que la memòria cau caduqui sola, algú la invalida explícitament.
Hi ha dues funcions, importades de next/cache:
| Funció | Què invalida | Quan fer-la servir |
|---|---|---|
revalidatePath(ruta) |
L'HTML en memòria cau d'una ruta concreta | Saps exactament quina pàgina canvia |
revalidateTag(etiqueta) |
Totes les peticions marcades amb aquesta etiqueta, estiguin on estiguin | La dada apareix en diverses pàgines |
Les etiquetes es declaren al propi fetch, i aquí hi ha la potència del mecanisme:
// src/consultes/aparador.js
const API = 'http://localhost:3001';
export async function obtenirBicicletes() {
const resposta = await fetch(`${API}/bicicletas`, {
next: { revalidate: 300, tags: ['bicicletas'] },
});
return resposta.json();
}
export async function obtenirBicicleta(bicicletaId) {
const resposta = await fetch(`${API}/bicicletas/${bicicletaId}`, {
next: { revalidate: 60, tags: ['bicicletas', `bicicleta-${bicicletaId}`] },
});
return resposta.ok ? resposta.json() : null;
}Fixa't que obtenirBicicleta porta dues etiquetes: una general i una específica. Això permet invalidar amb dues granularitats diferents: revalidateTag('bicicleta-bici-002') afecta només aquesta fitxa; revalidateTag('bicicletas') afecta la fitxa, el llistat i qualsevol altra pàgina que faci servir aquestes dades, sense que les hagis d'enumerar.
El disparador és un gestor de ruta (route.js), que a l'App Router és l'equivalent a un endpoint d'API. El panell intern de CicloUrbano el cridarà en desar una bicicleta:
// src/app/api/revalidar/route.js
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';
export async function POST(peticio) {
// 1. Autenticació: sense això, qualsevol pot tirar la teva memòria cau.
const secret = peticio.headers.get('x-secret-revalidacio');
if (secret !== process.env.SECRET_REVALIDACIO) {
return NextResponse.json({ error: 'No autoritzat' }, { status: 401 });
}
// 2. Què cal invalidar.
const { etiqueta, ruta } = await peticio.json();
if (etiqueta) revalidateTag(etiqueta);
if (ruta) revalidatePath(ruta);
return NextResponse.json({ revalidat: true, moment: Date.now() });
}I així s'invoca des del sistema de gestió quan algú edita bici-002:
curl -X POST http://localhost:3000/api/revalidar \
-H "Content-Type: application/json" \
-H "x-secret-revalidacio: $SECRET_REVALIDACIO" \
-d '{"etiqueta":"bicicleta-bici-002"}'Tres advertiments importants:
- L'autenticació no és opcional. Un endpoint de revalidació obert permet a qualsevol forçar renders en bucle: és una denegació de servei servida en safata.
revalidatePathamb una ruta dinàmica requereix el segon argument:revalidatePath('/bicicletas/[bicicletaId]', 'page')invalida totes les fitxes.- La invalidació no regenera immediatament: marca la memòria cau com a caducada. La regeneració la dispara la següent visita, com a l'apartat 9.
La mateixa funció revalidateTag es pot cridar des d'una acció de servidor, que és la forma natural de fer-ho quan el propi Next.js gestiona el formulari. Les accions de servidor són material de 10-03.
- Triar bé el temps de revalidació
El valor de revalidate no es tria a l'atzar. La pregunta correcta és: quants segons de desfasament entre la realitat i el que veu l'usuari són acceptables aquí?
| Contingut de CicloUrbano | Valor raonable | Raonament |
|---|---|---|
| Condicions del servei, «sobre nosaltres» | false (mai caduca) |
Canvia amb un desplegament; revalidar sota demanda si cal |
| Tarifes | 86400 (1 dia) |
Un canvi de preu es comunica; un dia de desfasament és tolerable |
| Llistat d'estacions (dades fixes) | 3600 (1 hora) |
Nom, barri i places totals gairebé no canvien |
| Fitxa de bicicleta | 60 (1 minut) |
Bon equilibri entre frescor i cost, amb etiqueta per forçar |
| Places lliures per estació | 60 (1 minut) |
Orientatiu per definició; ningú espera exactitud al segon |
| Catàleg amb disponibilitat en viu | 0 / SSR |
Aquí l'obsolescència sí que enganya l'usuari |
| Panell de taller | No aplica | Dades privades; va a la SPA |
Dues heurístiques útils:
- Comença generós i baixa si cal. Un valor alt amb revalidació sota demanda com a sortida d'emergència sol ser millor que un valor baix: costa menys i dona control exacte.
- Si el número correcte sembla ser 0, la ruta no és estàtica. Força-la a dinàmica i sigues explícit; no simulis SSR amb
revalidate: 1, perquè pagues la complexitat de les dues estratègies sense l'avantatge de cap.
- Contingut des de fitxers locals o des d'un CMS
Un component de servidor no està limitat a fetch. Com que s'executa a Node, pot llegir el sistema de fitxers, una cosa impensable a la SPA. És la forma més directa de gestionar les pàgines informatives de CicloUrbano en Markdown.
---
titol: Condicions del servei
descripcio: Condicions de lloguer de bicicletes de CicloUrbano.
actualitzat: 2026-03-14
---
## Facturació
El lloguer es factura per hores completes des del desbloqueig.// src/app/[pagina]/page.jsx — una ruta per a totes les informatives
import fs from 'node:fs/promises';
import path from 'node:path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
import { notFound } from 'next/navigation';
const DIRECTORI = path.join(process.cwd(), 'contingut');
export async function generateStaticParams() {
const fitxers = await fs.readdir(DIRECTORI);
return fitxers.map((fitxer) => ({ pagina: fitxer.replace(/\.md$/, '') }));
}
// Conjunt tancat: qualsevol altra URL ha de ser un 404.
export const dynamicParams = false;
async function llegirPagina(nom) {
try {
const brut = await fs.readFile(path.join(DIRECTORI, `${nom}.md`), 'utf8');
const { data, content } = matter(brut);
const processat = await remark().use(html).process(content);
return { meta: data, html: processat.toString() };
} catch {
return null;
}
}
export async function generateMetadata({ params }) {
const { pagina } = await params;
const contingut = await llegirPagina(pagina);
if (!contingut) return { title: 'Pàgina no trobada' };
return { title: contingut.meta.titol, description: contingut.meta.descripcio };
}
export default async function PaginaInformativa({ params }) {
const { pagina } = await params;
const contingut = await llegirPagina(pagina);
if (!contingut) notFound();
return (
<article>
<h1>{contingut.meta.titol}</h1>
<p><small>Actualitzat el {contingut.meta.actualitzat}</small></p>
<div dangerouslySetInnerHTML={{ __html: contingut.html }} />
</article>
);
}Punts a destacar:
fsen un component. Només funciona perquè és de servidor. Si hi afegissis'use client'a aquest fitxer, la construcció fallaria: no hi ha sistema de fitxers al navegador.dynamicParams = falseés el correcte aquí: el conjunt de pàgines informatives és tancat i una URL inventada ha de donar 404.dangerouslySetInnerHTMLés acceptable perquè el Markdown l'escriu l'equip, no un usuari anònim. Amb contingut de tercers caldria sanejar-lo abans.- Substituir els fitxers per un CMS (Contentful, Strapi, Sanity, WordPress headless) no canvia l'estructura: només canvia
llegirPaginaper unfetcha la seva API, i la revalidació sota demanda es dispara des del webhook del CMS alroute.jsde l'apartat 10.
- Rutes estàtiques especials:
sitemap, robots i les imatges de previsualització
sitemap, robots i les imatges de previsualitzacióNext.js reserva alguns noms de fitxer que generen artefactes estàtics a la construcció. Són els que completen la feina de SEO començada a 10-01.
El mapa del lloc. Un fitxer que exporta una funció per defecte i produeix sitemap.xml:
// src/app/sitemap.js
const BASE = 'https://ciclourbano.test';
export default async function sitemap() {
const [bicicletes, estacions] = await Promise.all([
fetch('http://localhost:3001/bicicletas').then((r) => r.json()),
fetch('http://localhost:3001/estaciones').then((r) => r.json()),
]);
const fixes = ['', '/estaciones', '/condiciones', '/tarifas'].map((ruta) => ({
url: `${BASE}${ruta}`,
lastModified: new Date(),
changeFrequency: ruta === '' ? 'hourly' : 'monthly',
priority: ruta === '' ? 1 : 0.6,
}));
const fitxes = bicicletes.map((bicicleta) => ({
url: `${BASE}/bicicletas/${bicicleta.id}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.8,
}));
const parades = estacions.map((estacio) => ({
url: `${BASE}/estaciones/${estacio.id}`,
changeFrequency: 'weekly',
priority: 0.7,
}));
return [...fixes, ...fitxes, ...parades];
}Les regles per als robots. Aquí es fa explícit el repartiment de l'arquitectura:
// src/app/robots.js
export default function robots() {
return {
rules: [
{
userAgent: '*',
allow: '/',
// El que viu a la SPA o és privat no s'ha de rastrejar.
disallow: ['/api/', '/taller', '/reservas'],
},
],
sitemap: 'https://ciclourbano.test/sitemap.xml',
};
}Les imatges de previsualització oberta, generades a la construcció. A 10-01 apuntàvem images: [{ url: '/imatges/bici-002.jpg' }] a un fitxer que algú havia de crear a mà. Next.js pot dibuixar-la amb ImageResponse:
// src/app/bicicletas/[bicicletaId]/opengraph-image.jsx
import { ImageResponse } from 'next/og';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export const alt = 'Bicicleta de CicloUrbano';
export default async function ImatgePrevisualitzacio({ params }) {
const { bicicletaId } = await params;
const bicicleta = await fetch(`http://localhost:3001/bicicletas/${bicicletaId}`)
.then((r) => r.json());
return new ImageResponse(
(
<div style={{
display: 'flex', flexDirection: 'column', justifyContent: 'center',
width: '100%', height: '100%', padding: 80,
background: '#12805c', color: '#ffffff', fontSize: 64,
}}>
<div style={{ fontSize: 32, opacity: 0.85 }}>CicloUrbano</div>
<div style={{ fontWeight: 700 }}>{bicicleta.model}</div>
<div style={{ fontSize: 40 }}>
{bicicleta.preuHora.toFixed(2)} €/h · {bicicleta.tipus}
</div>
</div>
),
size,
);
}Notes: el JSX de ImageResponse no és React del navegador —és una descripció que es converteix en PNG—, i per això només admet un subconjunt de CSS basat en Flexbox, amb display: 'flex' obligatori als contenidors. Com que conviu amb generateStaticParams, les imatges es generen a la construcció i se serveixen des del CDN, amb la qual cosa la targeta de WhatsApp de cada bicicleta és diferent sense feina de disseny per fitxa.
- Imatges i tipografies:
next/image i next/font
next/image i next/fontDues optimitzacions que l'aparador aprofita directament i que no existeixen al projecte Vite sense instal·lar res.
next/image substitueix <img> i resol d'una vegada diversos problemes de rendiment:
import Image from 'next/image';
<Image
src={`/imatges/${bicicleta.id}.jpg`}
alt={`Bicicleta ${bicicleta.model} de CicloUrbano`}
width={640}
height={480}
priority={esLaPrincipal}
sizes="(max-width: 768px) 100vw, 640px"
/>| Què fa | Per què importa |
|---|---|
| Converteix a WebP/AVIF i redimensiona sota demanda | Redueix el pes de la imatge de forma dràstica |
Genera srcset a partir de sizes |
El mòbil no descarrega la versió d'escriptori |
| Càrrega mandrosa per defecte | Les targetes de sota no competeixen amb les de dalt |
Reserva l'espai amb width/height |
Elimina el salt de contingut (CLS) |
priority desactiva la càrrega mandrosa i precarrega |
Millora el LCP a la imatge principal |
alt és obligatori; l'accessibilitat del mòdul 3 continua aplicant igual.
next/font autoallotja les tipografies a la construcció, sense peticions a servidors externs:
// src/app/layout.jsx (fragment)
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-base',
});
export default function PlantillaArrel({ children }) {
return (
<html lang="ca" className={inter.variable}>
<body>{children}</body>
</html>
);
}Això descarrega la tipografia a next build, la serveix des del teu propi domini (millor privacitat i una connexió menys), i genera els descriptors necessaris perquè el canvi de tipografia no mogui el text, que és una altra font clàssica de CLS.
- Quan no convé l'estàtic
L'estàtic és tan còmode que és fàcil passar-se. Aquests són els límits reals:
| Situació | Problema | Què fer |
|---|---|---|
| 200.000 pàgines que canvien diàriament | La construcció triga hores i es repeteix a cada desplegament | generateStaticParams amb les més visitades + dynamicParams: true |
| Contingut personalitzat per usuari | Un HTML per usuari no té sentit | SSR amb cookies(), o CSR dins d'una illa de client |
| Dades que han de ser exactes al segon | L'ISR serveix per definició contingut obsolet | SSR sense memòria cau |
| Cerca amb paràmetres lliures | Infinites combinacions de searchParams |
Ruta dinàmica, o filtrar al client |
| Preus, existències, saldos | Mostrar una dada caducada té conseqüències legals o econòmiques | SSR, o marcar la part crítica com a dinàmica |
| Contingut després d'autenticació | No es pot prerenderitzar el que depèn d'una sessió | SPA o SSR |
Hi ha un patró que resol la majoria d'aquests casos sense renunciar a l'estàtic: una pàgina estàtica amb un forat dinàmic. La fitxa de bici-002 es prerenderitza sencera —model, preu, descripció, imatge— i només el bloc de disponibilitat en viu s'omple a part, embolcallat en <Suspense>. S'obté el TTFB del CDN i l'exactitud del SSR a la mateixa pàgina. Aquest patró és el cor de 10-03, i és la raó que la pròxima lliçó sigui sobre el model i no sobre més funcions de Next.js.
- Taula resum definitiva i arquitectura híbrida
Tancament del mapa. Aquesta és la taula de les quatre estratègies aplicada pantalla per pantalla a CicloUrbano, amb la implementació concreta:
| Pantalla | Estratègia | Projecte | Implementació |
|---|---|---|---|
/ catàleg amb disponibilitat |
SSR | ciclourbano-web |
fetch(..., { cache: 'no-store' }) |
/bicicletas/[bicicletaId] |
ISR 60 s | ciclourbano-web |
generateStaticParams + next: { revalidate: 60, tags } |
/estaciones |
ISR 1-10 min | ciclourbano-web |
revalidate per fetch, dos ritmes |
/estaciones/[estacionId] (+pestanyes) |
ISR 5 min | ciclourbano-web |
generateStaticParams al layout |
/condiciones, /tarifas, /sobre-nosotros |
SSG | ciclourbano-web |
Markdown local + dynamicParams: false |
sitemap.xml, robots.txt, imatges OG |
SSG | ciclourbano-web |
sitemap.js, robots.js, opengraph-image.jsx |
/acceso |
CSR | SPA Vite | PaginaAcces + sliceSessio |
/reservas, /reservas/nueva |
CSR | SPA Vite | TanStack Query + sliceReserves |
/taller |
CSR | SPA Vite | RutaProtegida + RequereixRol |
I així queda l'arquitectura completa:
flowchart TB
subgraph PUBLIC["Aparador public · ciclourbano-web (Next.js 15)"]
CDN["CDN"]
SSG["SSG: condicions, tarifes, sitemap, robots"]
ISR["ISR: fitxes de bicicleta, estacions"]
SSR["SSR: cataleg amb disponibilitat"]
end
subgraph GESTIO["Aplicacio de gestio · SPA (Vite + React Router)"]
SPA["CSR: acces, reserves, taller"]
RTK["Redux Toolkit + TanStack Query"]
end
API[("API · json-server<br/>localhost:3001")]
SSG --> CDN
ISR --> CDN
SSR --> API
ISR --> API
SPA --> RTK --> API
CDN --> U1["Visitant anonim<br/>i cercadors"]
SSR --> U1
SPA --> U2["Usuari identificat<br/>Ana Ribera / Marc Sole"]
U1 -. "Iniciar sessio" .-> SPA
Aquesta arquitectura és habitual en producció i mereix anomenar-se: l'aparador i l'aplicació són dos projectes, dos desplegaments i dos models de renderitzat, units per una API comuna i per una cookie de sessió compartida. No és una transició a mitges cap a Next.js: és la decisió correcta, perquè les dues parts tenen requisits oposats. El que és públic necessita ser ràpid, indexable i resistent; el que és privat necessita ser interactiu i estar sempre al dia.
Sobre el projecte final. Convé dir-ho aquí amb claredat perquè no hi hagi confusió: el Mòdul 11 construeix l'aplicació completa amb Vite + React Router, la que portes nou mòduls muntant. Next.js és una eina que ara ja saps fer servir i situar, no un canvi de rumb del curs. Quan en un projecte real hagis de decidir, tindràs el criteri de la taula de dalt.
Errors Comuns i Consells
- Provar l'ISR amb
npm run dev. En desenvolupament, Next.js renderitza sempre i no guarda res en memòria cau. L'ISR només es comprova ambnpm run build && npm run start. - Esperar que
revalidate: 60es regeneri sola cada minut. No hi ha temporitzador: la regeneració la dispara una visita després de la caducitat. Sense trànsit, no hi ha regeneració. - Afegir
console.logper depurar i trobar-lo buit. Si la pàgina és estàtica, aquestconsole.loges va executar un cop, durantnext build, i la seva sortida és al registre de la construcció, no al del servidor. - Retornar claus amb el nom equivocat a
generateStaticParams. Si la carpeta és[bicicletaId], la clau ha de serbicicletaId. I el valor, una cadena. - Fer servir
searchParamssense adonar-te que trenca el prerenderitzat. Una sola lectura converteix la pàgina enƒ. Si necessites paràmetres de consulta sense perdre l'estàtic, llegeix-los al client ambuseSearchParams. - Deixar
/api/revalidarsense autenticació. És un vector de denegació de servei: qualsevol pot forçar renders en bucle. Secret a la capçalera, sempre. - Fixar
revalidate: 1«per si de cas». És SSR amb complexitat extra. Si necessites frescor absoluta, declara la ruta dinàmica i sigues explícit. - Oblidar
widthiheightanext/image. Sense ells no es reserva espai i torna el salt de contingut que el component venia a evitar. - Consell: fes de
next buildpart de la teva integració contínua. Combinat ambexport const dynamic = 'error'a les rutes que han de ser estàtiques, unfetchmal configurat deixa de ser un problema de producció i passa a ser una fallada de construcció. - Consell: prerenderitza només el que es visita. En catàlegs grans,
generateStaticParamsamb el més popular idynamicParams: truedona el 95 % del benefici amb una fracció del temps de construcció.
Exercicis
Exercici 1. Aquest next build no és l'esperat. Les tres primeres rutes haurien de ser estàtiques o SSG i apareixen com a dinàmiques. Per a cadascuna, proposa la causa més probable i la correcció.
Route (app) Size First Load JS ┌ ƒ /condiciones 136 B 88 kB ├ ƒ /bicicletas/[bicicletaId] 0.9 kB 101 kB ├ ƒ /estaciones 310 B 89 kB └ ƒ / 1.2 kB 102 kB
Saps a més que: /condiciones només conté text en JSX; /bicicletas/[bicicletaId] té generateStaticParams; /estaciones fa dos fetch sense opcions; i / llegeix searchParams. Indica també quina de les quatre ha de continuar sent ƒ.
Exercici 2. Dissenya l'estratègia de memòria cau de /estaciones/[estacionId], que mostra: nom, barri i places totals (fixos); la flota aparcada ara mateix (canvia cada pocs minuts); i les incidències obertes (s'han de veure tan bon punt un operari en dona d'alta una). Escriu el codi de les peticions amb els seus revalidate i les seves tags, i explica com es força l'actualització de les incidències sense esperar.
Exercici 3. L'equip de CicloUrbano vol una secció de blog a l'aparador: articles en Markdown a contingut/blog/, llistat a /blog i detall a /blog/[slug]. Els articles es publiquen un cop o dos al mes. Decideix l'estratègia de renderitzat, el valor de dynamicParams i què cal afegir al sitemap.js, i justifica cada decisió en una frase.
Solucions
Solució 1.
| Ruta | Causa probable | Correcció |
|---|---|---|
/condiciones |
Alguna cosa global la contamina: gairebé segur un await cookies() a layout.jsx (el MenuUsuari de 10-01), o un export const dynamic = 'force-dynamic' heretat |
Treure la lectura de la cookie del layout de servidor: llegir-la en un component de client o aïllar-la en un component embolcallat en <Suspense> |
/bicicletas/[bicicletaId] |
Té generateStaticParams, però els fetch van sense memòria cau: a Next.js 15 això n'hi ha prou per fer-la dinàmica |
Canviar a next: { revalidate: 60, tags: [...] } |
/estaciones |
Mateix motiu: fetch sense opcions ja no es cacheja a Next.js 15 |
Afegir next: { revalidate: N } a cada petició |
/ |
Llegeix searchParams i mostra disponibilitat en viu |
Ha de continuar sent ƒ. És el cas legítim de SSR de tot l'aparador |
L'aprenentatge general: una ruta que sembla estàtica i surt com a ƒ gairebé sempre té la causa en un ancestre o en una opció de fetch oblidada.
Solució 2.
// src/app/estaciones/[estacionId]/page.jsx
const API = 'http://localhost:3001';
export async function generateStaticParams() {
const estacions = await fetch(`${API}/estaciones`).then((r) => r.json());
return estacions.map((estacio) => ({ estacionId: estacio.id }));
}
export default async function PestanyaFlota({ params }) {
const { estacionId } = await params;
const [estacio, flota, incidencies] = await Promise.all([
// 1. Dades fixes: una hora és més que suficient.
fetch(`${API}/estaciones/${estacionId}`, {
next: { revalidate: 3600, tags: [`estacio-${estacionId}`] },
}).then((r) => r.json()),
// 2. Flota aparcada: canvia cada pocs minuts.
fetch(`${API}/bicicletas?estacioId=${estacionId}`, {
next: { revalidate: 120, tags: ['bicicletas', `flota-${estacionId}`] },
}).then((r) => r.json()),
// 3. Incidències: caducitat llarga perquè s'invaliden sota demanda.
fetch(`${API}/incidencias?estacioId=${estacionId}`, {
next: { revalidate: 3600, tags: [`incidencies-${estacionId}`] },
}).then((r) => r.json()),
]);
return (
<>
<p>{estacio.barri} · {estacio.places} places</p>
<p>{flota.length} bicicletes aparcades</p>
<p>{incidencies.length} incidències obertes</p>
</>
);
}La clau està en la tercera petició: no s'usa un revalidate curt, sinó un de llarg amb una etiqueta. Quan un operari dona d'alta una incidència des del panell de taller, el sistema de gestió crida l'endpoint de revalidació:
curl -X POST http://localhost:3000/api/revalidar \
-H "x-secret-revalidacio: $SECRET_REVALIDACIO" \
-H "Content-Type: application/json" \
-d '{"etiqueta":"incidencies-est-02"}'Així s'obté el millor de les dues estratègies: cost mínim en règim normal (una regeneració per hora com a molt) i actualització immediata quan de veritat hi ha alguna cosa nova. Baixar revalidate a 10 segons donaria un resultat pitjor i més car.
Solució 3.
- Estratègia: SSG pur per al llistat i per al detall. Els articles són fitxers del repositori, així que qualsevol canvi implica un desplegament, i el desplegament ja reconstrueix el lloc: una revalidació per temps no aportaria res.
dynamicParams = falsea/blog/[slug]: el conjunt d'articles està tancat en el moment de la construcció, i un slug inventat ha de retornar un 404 real en lloc de disparar un render.generateStaticParamsllegeixcontingut/blog/ambfs.readdiri retorna un{ slug }per fitxer.- A
sitemap.jscal afegir la ruta/blogi una entrada per article, amblastModifiedpres del campdatadel front matter —no denew Date(), que marcaria tots els articles com a acabats de modificar a cada construcció i degradaria el senyal per als cercadors— ichangeFrequency: 'monthly'. - Extra recomanable: un
opengraph-image.jsxa/blog/[slug]amb el títol de l'article, perquè compartir un enllaç en un xat mostri una targeta pròpia.
Si en el futur els articles es mouen a un CMS, l'estratègia passaria a ISR amb revalidació sota demanda des del webhook del CMS: la resta del codi no canviaria.
Conclusió
Amb aquesta lliçó queda complet el mapa de renderitzat que obria el mòdul. A 10-01 l'HTML es generava a cada petició; aquí es genera una vegada, a next build, se serveix des d'un CDN i —amb ISR— es refresca només quan cal.
L'essencial de l'estratègia. A l'App Router tota ruta és estàtica per defecte i es torna dinàmica en el moment que fa servir alguna cosa que només es coneix en la petició: cookies(), headers(), searchParams o un fetch sense memòria cau —que a Next.js 15 és el comportament per defecte, així que la memòria cau cal demanar-la—. I no se suposa res: next build ho diu, amb ○ per a estàtic, ● per a SSG amb generateStaticParams i ƒ per a dinàmic. Aquesta comanda pertany a la rutina prèvia al desplegament tant com npm test.
De les eines, en queden fixades quatre. generateStaticParams enumera els valors d'un segment dinàmic i genera una pàgina per cadascun, amb la clau anomenada exactament com la carpeta. dynamicParams decideix què passa amb el que no hi era: true ho renderitza sota demanda i ho desa en memòria cau —el correcte per a un catàleg que creix—, false retorna un 404 —el correcte per a un conjunt tancat—. revalidate, per ruta o per petició, implementa el patró stale-while-revalidate: ningú espera mai, algú veu contingut lleugerament antic, i la caducitat no és un temporitzador sinó una condició que dispara la següent visita. I revalidateTag / revalidatePath inverteixen el control per als casos en què esperar no compensa: etiquetes declarades al fetch, un route.js autenticat i una crida des del sistema de gestió. La combinació guanyadora, que has vist a l'exercici 2, és caducitat llarga més etiqueta: cost mínim en repòs i actualització immediata quan hi ha novetat.
L'aparador ha guanyat a més la seva capa de SEO i rendiment completa: sitemap.js amb les fitxes i les estacions, robots.js que exclou el que viu a la SPA, opengraph-image.jsx que dibuixa una targeta diferent per bicicleta a la construcció, next/image amb el seu srcset, la seva càrrega mandrosa i la seva reserva d'espai, i next/font autoallotjant la tipografia sense salts de text. I les pàgines informatives surten de fitxers Markdown llegits amb fs des d'un component de servidor, una cosa que a la SPA era senzillament impossible.
L'arquitectura de CicloUrbano queda tancada i és deliberadament híbrida: aparador públic a ciclourbano-web —catàleg en SSR, fitxes i estacions en ISR, informatives en SSG— i aplicació de gestió a la SPA de Vite —accés, reserves i taller en CSR amb Redux Toolkit i TanStack Query—, unides per l'API comuna i per una cookie de sessió compartida. I amb això, el recordatori que ja s'ha fet a dalt: el projecte final del Mòdul 11 es construeix amb Vite + React Router.
Queda una peça sense explicar, i és precisament la que sosté tot l'anterior. Has escrit 'use client' sense saber del tot què significa; has acceptat que un component de servidor «no s'envia al navegador» sense veure com; has posat un loading.jsx sabent que per sota hi ha un <Suspense>; i l'apartat 15 ha acabat prometent el patró que resol gairebé tots els casos difícils: una pàgina estàtica amb un forat dinàmic a dins. La pròxima lliçó deixa de banda la sintaxi de Next.js i va al model: què és exactament un límit de suspensió, com un component «se suspèn», com el servidor envia l'HTML per parts, on s'executa cada component i què pot i no pot travessar la frontera entre servidor i client. La pròxima lliçó és Suspense i React Server Components.
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
