La lección anterior dejó el escaparate ciclourbano-web funcionando con renderizado dinámico: cada visita provoca un render en el servidor. Para el catálogo con disponibilidad en vivo eso es lo correcto. Para la página de condiciones del servicio, que cambia dos veces al año, es un despilfarro difícil de justificar: el servidor genera exactamente el mismo HTML miles de veces al día, consume CPU, añade latencia y puede caerse. Esta lección va al otro extremo de la tabla del módulo —generar el HTML una sola vez, en la construcción— y después busca el punto intermedio que resuelve la mayoría de los casos reales: la regeneración incremental. Aprenderás a decidir qué contenido gana con ser estático, a hacer que Next.js prerenderice rutas dinámicas como la ficha de cada bicicleta, a leer la salida de next build para saber qué ha hecho realmente, a caducar y refrescar el HTML por tiempo o bajo demanda, y a cerrar la arquitectura híbrida de CicloUrbano: escaparate estático y aplicación de gestión en la SPA.
Contenido
- El desperdicio de renderizar mil veces lo mismo
- Qué es exactamente la generación estática
- Inventario de CicloUrbano: qué gana con ser estático y qué no
- Cómo decide Next.js entre estático y dinámico
- Leer la salida de
next build: ○, ● y ƒ generateStaticParams: prerenderizar rutas dinámicasdynamicParams: qué pasa con un identificador nuevo- Revalidación incremental (ISR)
- Obsoleto mientras se revalida: la línea temporal
- Revalidación bajo demanda:
revalidatePathyrevalidateTag - Elegir bien el tiempo de revalidación
- Contenido desde ficheros locales o desde un CMS
- Rutas estáticas especiales:
sitemap,robotsy las imágenes de previsualización - Imágenes y fuentes:
next/imageynext/font - Cuándo no conviene el estático
- Tabla resumen definitiva y arquitectura híbrida
- El desperdicio de renderizar mil veces lo mismo
Pon números al escaparate de CicloUrbano. Supón 50.000 visitas diarias a la página de condiciones del servicio, y que renderizarla cuesta 40 ms de CPU en el servidor.
| Renderizado dinámico (SSR) | Generación estática (SSG) | |
|---|---|---|
| Renders al día | 50.000 | 0 (uno en la construcción) |
| CPU consumida | ~33 minutos diarios | ~40 ms, una vez |
| TTFB típico | 80-250 ms | 10-30 ms desde el CDN |
| ¿Sobrevive si la API cae? | No | Sí |
| ¿Sobrevive a un pico de tráfico? | Solo escalando | Sí, el CDN absorbe |
| Coste de alojamiento | Servidor Node siempre encendido | Ficheros en un CDN |
Todas las filas apuntan en la misma dirección y el motivo es uno solo: el resultado no depende de la petición. Cuando el HTML es idéntico para todo el mundo y no cambia entre visitas, generarlo cada vez es trabajo repetido.
Hay una diferencia adicional que no aparece en la tabla y que suele decidir la arquitectura: la resiliencia. Una página estática ya está escrita en el disco de un CDN. Si json-server deja de responder, la ficha de bici-002 sigue sirviéndose con los últimos datos conocidos. Con SSR, esa misma ficha devuelve un 500. Para el escaparate público de una empresa, que la web siga en pie cuando el sistema interno falla no es un lujo: es lo que se espera.
- Qué es exactamente la generación estática
La generación estática consiste en ejecutar los componentes durante next build, guardar el HTML resultante en disco y servir ese fichero a todo el mundo.
flowchart LR
subgraph CONSTRUCCION["next build · una vez"]
A["Componentes de servidor"] --> B["fetch a la API"]
B --> C["React renderiza"]
C --> D["HTML + payload RSC<br/>guardados en disco"]
end
D --> E["CDN"]
E --> F["Visitante 1"]
E --> G["Visitante 2"]
E --> H["Visitante 50.000"]
Tres precisiones que evitan malentendidos habituales:
- Estático no significa «sin React». El HTML prerenderizado se hidrata igual que el del SSR, y las islas de cliente (
SelectorTipo,BotonTema) siguen siendo interactivas. Lo único que cambia es cuándo se generó ese HTML. - Estático no significa «sin datos». Los
fetchse ejecutan; simplemente se ejecutan en la máquina de construcción, no en cada petición. - Estático no significa «para siempre». Con ISR, ese HTML caduca y se regenera. Es lo que veremos a partir del apartado 8.
El código de una página estática es idéntico al de una dinámica. No hay una API distinta: lo único que cambia es si usas algo que obligue a esperar a la petición.
// src/app/condiciones/page.jsx — página estática sin hacer nada especial
export const metadata = {
title: 'Condiciones del servicio',
description: 'Condiciones de alquiler de bicicletas de CicloUrbano.',
};
export default function PaginaCondiciones() {
return (
<article>
<h1>Condiciones del servicio</h1>
<p>El alquiler se factura por horas completas desde el momento del desbloqueo.</p>
<h2>Fianzas</h2>
<p>Las bicicletas de carga requieren una fianza de 30 € reembolsable.</p>
</article>
);
}Esta página no lee cookies, no lee searchParams y no hace ningún fetch sin caché. Next.js la prerenderiza sin que se lo pidas.
- Inventario de CicloUrbano: qué gana con ser estático y qué no
El criterio se puede reducir a dos preguntas: ¿el resultado depende de quién pide la página? y ¿con qué frecuencia cambia?
| Pantalla | ¿Depende del usuario? | Frecuencia de cambio | Estrategia | Por qué |
|---|---|---|---|---|
/condiciones, /sobre-nosotros, /tarifas |
No | Dos veces al año | SSG puro | Texto fijo; regenerarlo en cada visita no aporta nada |
/estaciones (listado) |
No | Alta o baja según qué se muestre | ISR 5 min | Nombre y barrio son fijos; las plazas libres cambian |
/bicicletas/[bicicletaId] (ficha) |
No | Varias veces al día | ISR 60 s | Cientos de páginas, cambios moderados, mucho valor de SEO |
/ (catálogo con disponibilidad en vivo) |
No, pero debe ser exacto | Constante | SSR | Mostrar disponibilidad obsoleta engaña al usuario |
/reservas |
Sí | Constante | CSR (SPA) | Datos privados; nada que prerenderizar |
/taller |
Sí, además por rol | Constante | CSR (SPA) | Detrás de RutaProtegida; ningún valor de SEO |
Observa el matiz de la ficha de bicicleta frente al catálogo. Los dos muestran el estado de la bicicleta, pero el compromiso es distinto: el catálogo promete disponibilidad ahora mismo y por eso no puede permitirse datos de hace un minuto; la ficha es sobre todo una página de contenido —modelo, tipo, precio, estación—, y un desfase de 60 segundos en la etiqueta de estado es aceptable si a cambio se sirve desde el CDN. La decisión no es técnica, es de producto: cuánta obsolescencia tolera cada pantalla.
- Cómo decide Next.js entre estático y dinámico
La regla del App Router se enuncia en una frase:
Toda ruta es estática por defecto. Se vuelve dinámica en cuanto usa algo que solo se conoce en el momento de la petición.
Esos «algo» son exactamente los detonantes que ya viste en 10-01, ahora en su papel completo:
| Detonante | Efecto | Cómo evitarlo si quieres estático |
|---|---|---|
fetch(..., { cache: 'no-store' }) |
Dinámica | cache: 'force-cache' o next: { revalidate: N } |
Un fetch sin opciones (Next.js 15) |
Dinámica | Igual que el anterior: la caché hay que pedirla |
await cookies() |
Dinámica | Aislar en un componente de cliente o dentro de <Suspense> |
await headers(), connection() |
Dinámica | Igual |
La prop searchParams de una page |
Dinámica | Leer el parámetro en un componente de cliente con useSearchParams |
export const dynamic = 'force-dynamic' |
Dinámica | Quitarlo |
export const revalidate = 0 |
Dinámica | Poner un valor mayor que cero |
Y los dos interruptores explícitos que conviene conocer:
// Fuerza el comportamiento de toda la ruta. Valores posibles:
export const dynamic = 'auto'; // por defecto: Next.js decide
export const dynamic = 'force-static'; // fuerza estático (cookies() devuelve vacío)
export const dynamic = 'force-dynamic'; // fuerza render en cada petición
export const dynamic = 'error'; // estático, y ERROR si algo lo hace dinámicodynamic = 'error' es una herramienta excelente en un proyecto real: convierte «creía que esta página era estática» en un fallo de construcción. Es la misma filosofía del análisis estático del módulo 9 aplicada al renderizado.
Un aviso importante sobre searchParams: usarlo en una page la hace dinámica entera, porque el servidor necesita la URL completa. Si solo quieres reaccionar a ?tipo= sin perder el prerenderizado, la solución es leer el parámetro en un componente de cliente y filtrar allí, o poner cada filtro en su propia ruta (/catalogo/electricas).
- Leer la salida de
next build: ○, ● y ƒ
next build: ○, ● y ƒNo hay que suponer nada: next build dice exactamente qué ha hecho con 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
Cómo se lee esta tabla:
| Símbolo | Significado | Cuándo se genera el HTML |
|---|---|---|
| ○ Static | Ruta sin parámetros, prerenderizada | En next build |
| ● SSG | Ruta dinámica prerenderizada con generateStaticParams |
En next build, una página por parámetro |
| ƒ Dynamic | Renderizada en el servidor en cada petición | En cada visita |
Y las columnas:
- Size: el JavaScript propio de esa ruta.
- First Load JS: el total que descarga quien entra por ahí, incluido el código compartido. Es la cifra del módulo 8 y sigue importando.
- Revalidate: el tiempo de caducidad, si lo hay.
Las líneas indentadas bajo ● son las páginas concretas generadas. Si esperabas una ○ y ves una ƒ, algo ha activado un detonante: revisa cookies(), searchParams y las opciones de fetch. Este comando debería formar parte de tu rutina antes de desplegar, igual que npm test.
generateStaticParams: prerenderizar rutas dinámicas
generateStaticParams: prerenderizar rutas dinámicasUna ruta como /bicicletas/[bicicletaId] tiene un problema evidente: en la construcción, Next.js no sabe qué identificadores existen. Puede prerenderizar /condiciones porque es una sola página, pero no puede adivinar que hay una bici-001 y una bici-002.
generateStaticParams es la función que se lo dice. Se ejecuta durante la construcción, devuelve la lista de valores posibles del segmento dinámico, y Next.js genera una página por cada uno.
// src/app/bicicletas/[bicicletaId]/page.jsx
import { notFound } from 'next/navigation';
import EtiquetaEstado from '@/componentes/EtiquetaEstado';
const API = 'http://localhost:3001';
export async function generateStaticParams() {
const respuesta = await fetch(`${API}/bicicletas`);
const bicicletas = await respuesta.json();
// Cada objeto del array corresponde a un [bicicletaId] del nombre de carpeta.
return bicicletas.map((bicicleta) => ({ bicicletaId: bicicleta.id }));
}
async function obtenerBicicleta(bicicletaId) {
const respuesta = await fetch(`${API}/bicicletas/${bicicletaId}`, {
next: { revalidate: 60, tags: [`bicicleta-${bicicletaId}`] },
});
return respuesta.ok ? respuesta.json() : null;
}
export async function generateMetadata({ params }) {
const { bicicletaId } = await params;
const bicicleta = await obtenerBicicleta(bicicletaId);
if (!bicicleta) return { title: 'Bicicleta no encontrada' };
return {
title: `${bicicleta.modelo} · ${bicicleta.precioHora.toFixed(2)} €/h`,
description: `Bicicleta ${bicicleta.tipo} disponible para alquiler por horas.`,
};
}
export default async function PaginaFichaBicicleta({ params }) {
const { bicicletaId } = await params;
const bicicleta = await obtenerBicicleta(bicicletaId);
if (!bicicleta) notFound();
return (
<article>
<h1>{bicicleta.modelo}</h1>
<EtiquetaEstado estado={bicicleta.estado} />
<p>{bicicleta.precioHora.toFixed(2)} €/h · estación {bicicleta.estacionId}</p>
</article>
);
}Análisis línea a línea de lo que ha cambiado respecto a 10-01:
- La forma del valor devuelto importa. Cada objeto debe tener una clave con el nombre exacto del segmento: la carpeta es
[bicicletaId], así que la clave esbicicletaId. Y el valor es siempre una cadena, aunque el identificador fuese numérico. cache: 'no-store'ha desaparecido y en su lugar haynext: { revalidate: 60 }. Esto es lo que permite que la ruta sea estática (●en el build) y a la vez se refresque cada minuto.- La etiqueta
tags: ['bicicleta-<id>']no hace nada por sí sola; es el anclaje para la revalidación bajo demanda del apartado 10. generateStaticParamsygenerateMetadatay el componente comparten losfetchgracias a la deduplicación automática que ya viste.
En rutas anidadas, cada nivel dinámico aporta su función. Para /estaciones/[estacionId]/incidencias, el generateStaticParams de [estacionId] se hereda y no hay que repetirlo:
// src/app/estaciones/[estacionId]/layout.jsx
export async function generateStaticParams() {
const respuesta = await fetch('http://localhost:3001/estaciones');
const estaciones = await respuesta.json();
return estaciones.map((estacion) => ({ estacionId: estacion.id }));
}Con las tres estaciones del canon, la construcción genera est-01, est-02 y est-03, cada una con su pestaña índice y su pestaña de incidencias: seis páginas.
dynamicParams: qué pasa con un identificador nuevo
dynamicParams: qué pasa con un identificador nuevoPregunta inevitable: el equipo de CicloUrbano da de alta bici-006 después del despliegue. Esa página no existe en el disco. ¿Qué ocurre cuando alguien la visita?
Lo decide dynamicParams:
| Valor | Comportamiento ante un parámetro no generado |
|---|---|
true (por defecto) |
Se renderiza en el servidor la primera vez, se guarda en caché y las siguientes visitas se sirven desde ella |
false |
Devuelve un 404 directamente, sin llamar al componente |
Cuándo usar cada uno:
truepara catálogos que crecen: bicicletas, estaciones, artículos. Es el comportamiento deseado en CicloUrbano: la nueva bicicleta funciona al instante, solo que la primera visita paga el render.falsecuando el conjunto es cerrado y conocido: idiomas soportados, categorías fijas, páginas legales. Así una URL inventada no dispara un render inútil, algo que además cierra una vía de abuso.
Con dynamicParams = true, generateStaticParams deja de ser una lista exhaustiva y pasa a ser una lista de precalentamiento. Un patrón muy útil en catálogos grandes es prerenderizar solo lo más visitado y dejar que el resto se genere bajo demanda:
export async function generateStaticParams() {
const respuesta = await fetch('http://localhost:3001/bicicletas');
const bicicletas = await respuesta.json();
// Solo las disponibles: son las que la gente consulta y comparte.
return bicicletas
.filter((bicicleta) => bicicleta.estado === 'disponible')
.map((bicicleta) => ({ bicicletaId: bicicleta.id }));
}Con el canon de CicloUrbano, esto genera bici-001, bici-004 y bici-005 en la construcción; bici-002 (alquilada) y bici-003 (en mantenimiento) se generarán la primera vez que alguien las pida.
- Revalidación incremental (ISR)
Aquí está la idea que hace utilizable la generación estática en un sitio con datos reales, y es la cuarta columna de la tabla del módulo.
ISR (Incremental Static Regeneration) sirve la página estática guardada, y cuando esa página lleva más de N segundos sin regenerarse, la vuelve a generar en segundo plano. El visitante que llega justo después de la caducidad no espera: recibe la versión antigua, y la nueva queda lista para el siguiente.
Se declara de dos formas, que se pueden combinar:
// A) Por ruta: afecta a toda la página.
export const revalidate = 300; // segundos
// B) Por petición: cada fetch tiene su propio ciclo de vida.
const respuesta = await fetch(`${API}/bicicletas/${id}`, {
next: { revalidate: 60 },
});Diferencias y cuál usar:
export const revalidate = N |
next: { revalidate: N } |
|
|---|---|---|
| Alcance | Toda la ruta | Una petición concreta |
| Granularidad | Baja | Alta |
| Cuándo usarlo | La página tiene un ritmo único | Datos con ritmos distintos en la misma página |
Cuando conviven, gana el más corto: si la ruta declara 300 y un fetch declara 60, la página se regenera cada 60 segundos.
Ejemplo real con dos ritmos en la misma página, el listado de estaciones:
// src/app/estaciones/page.jsx
import TarjetaEstacion from '@/componentes/TarjetaEstacion';
const API = 'http://localhost:3001';
// Tope de la página: como mucho, 10 minutos de obsolescencia.
export const revalidate = 600;
export default async function PaginaEstaciones() {
const [estaciones, ocupacion] = await Promise.all([
// El nombre y el barrio de una estación no cambian nunca: una hora está bien.
fetch(`${API}/estaciones`, { next: { revalidate: 3600 } }).then((r) => r.json()),
// Las plazas libres cambian constantemente: un minuto.
fetch(`${API}/ocupacion`, { next: { revalidate: 60 } }).then((r) => r.json()),
]);
return (
<section>
<h1>Nuestras estaciones</h1>
<ul>
{estaciones.map((estacion) => (
<li key={estacion.id}>
<TarjetaEstacion
estacion={estacion}
plazasLibres={ocupacion[estacion.id] ?? 0}
/>
</li>
))}
</ul>
</section>
);
}En el build, esta ruta aparece como ● con Revalidate: 1m —el mínimo efectivo—, y la página se sirve desde el CDN salvo la regeneración ocasional.
- Obsoleto mientras se revalida: la línea temporal
Este es el mecanismo exacto, y merece entenderlo bien porque explica una confusión muy común: «he cambiado el dato, he recargado y sigo viendo lo viejo». No es un fallo; es el diseño.
Con revalidate = 60 y una regeneración que tarda 2 segundos:
sequenceDiagram
participant V1 as Visitante A (t=0s)
participant V2 as Visitante B (t=75s)
participant V3 as Visitante C (t=78s)
participant N as Next.js
participant API as API
V1->>N: GET /bicicletas/bici-002
N-->>V1: HTML cacheado (fresco) · rapido
Note over N: Cache generada en t=0. Caduca en t=60.
V2->>N: GET /bicicletas/bici-002
N-->>V2: HTML cacheado (OBSOLETO) · rapido
Note over N: Caducada: se lanza la regeneracion EN SEGUNDO PLANO
N->>API: GET /bicicletas/bici-002
API-->>N: JSON actualizado
Note over N: Nueva cache lista en t=77
V3->>N: GET /bicicletas/bici-002
N-->>V3: HTML NUEVO · rapido
Lo que hay que retener:
- Nadie espera nunca. Ni siquiera el visitante B, que es quien dispara la regeneración. Este patrón se llama stale-while-revalidate.
- El visitante B ve contenido obsoleto, y es una decisión consciente: se prefiere una respuesta rápida y ligeramente antigua a una respuesta lenta y exacta.
revalidate: 60no significa «se regenera cada 60 segundos». Significa «pasados 60 segundos, la próxima visita dispara la regeneración». Si nadie entra en tres días, no se regenera nada. La caché es perezosa, no un cronómetro.- La regeneración es única. Si llegan cien visitantes a la vez tras la caducidad, se lanza una sola regeneración; los cien reciben la versión antigua.
De aquí sale una consecuencia práctica: en desarrollo (npm run dev) no verás este comportamiento, porque el modo de desarrollo renderiza siempre. Para comprobar el ISR de verdad hay que ejecutar npm run build && npm run start. Es la misma disciplina que en el módulo 8 con el Profiler: se mide sobre el build de producción.
- Revalidación bajo demanda:
revalidatePath y revalidateTag
revalidatePath y revalidateTagEl ISR por tiempo resuelve la mayoría de los casos, pero deja un hueco: cuando el equipo de CicloUrbano da de alta bici-006 o baja el precio de las eléctricas, no quiere esperar 60 segundos ni 10 minutos. Quiere que el cambio se refleje ya.
La revalidación bajo demanda invierte el control: en lugar de que la caché caduque sola, alguien la invalida explícitamente.
Hay dos funciones, importadas de next/cache:
| Función | Qué invalida | Cuándo usarla |
|---|---|---|
revalidatePath(ruta) |
El HTML cacheado de una ruta concreta | Sabes exactamente qué página cambia |
revalidateTag(etiqueta) |
Todas las peticiones marcadas con esa etiqueta, estén donde estén | El dato aparece en varias páginas |
Las etiquetas se declaran en el propio fetch, y ahí está la potencia del mecanismo:
// src/consultas/escaparate.js
const API = 'http://localhost:3001';
export async function obtenerBicicletas() {
const respuesta = await fetch(`${API}/bicicletas`, {
next: { revalidate: 300, tags: ['bicicletas'] },
});
return respuesta.json();
}
export async function obtenerBicicleta(bicicletaId) {
const respuesta = await fetch(`${API}/bicicletas/${bicicletaId}`, {
next: { revalidate: 60, tags: ['bicicletas', `bicicleta-${bicicletaId}`] },
});
return respuesta.ok ? respuesta.json() : null;
}Fíjate en que obtenerBicicleta lleva dos etiquetas: una general y una específica. Eso permite invalidar con dos granularidades distintas: revalidateTag('bicicleta-bici-002') afecta solo a esa ficha; revalidateTag('bicicletas') afecta a la ficha, al listado y a cualquier otra página que use esos datos, sin que tengas que enumerarlas.
El disparador es un manejador de ruta (route.js), que en el App Router es el equivalente a un endpoint de API. El panel interno de CicloUrbano lo llamará al guardar una bicicleta:
// src/app/api/revalidar/route.js
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';
export async function POST(peticion) {
// 1. Autenticación: sin esto, cualquiera puede tirar tu caché.
const secreto = peticion.headers.get('x-secreto-revalidacion');
if (secreto !== process.env.SECRETO_REVALIDACION) {
return NextResponse.json({ error: 'No autorizado' }, { status: 401 });
}
// 2. Qué hay que invalidar.
const { etiqueta, ruta } = await peticion.json();
if (etiqueta) revalidateTag(etiqueta);
if (ruta) revalidatePath(ruta);
return NextResponse.json({ revalidado: true, momento: Date.now() });
}Y así se invoca desde el sistema de gestión cuando alguien edita bici-002:
curl -X POST http://localhost:3000/api/revalidar \
-H "Content-Type: application/json" \
-H "x-secreto-revalidacion: $SECRETO_REVALIDACION" \
-d '{"etiqueta":"bicicleta-bici-002"}'Tres advertencias importantes:
- La autenticación no es opcional. Un endpoint de revalidación abierto permite a cualquiera forzar renders en bucle: es una denegación de servicio servida en bandeja.
revalidatePathcon una ruta dinámica requiere el segundo argumento:revalidatePath('/bicicletas/[bicicletaId]', 'page')invalida todas las fichas.- La invalidación no regenera de inmediato: marca la caché como caducada. La regeneración la dispara la siguiente visita, como en el apartado 9.
La misma función revalidateTag se puede llamar desde una acción de servidor, que es la forma natural de hacerlo cuando el propio Next.js gestiona el formulario. Las acciones de servidor son material de 10-03.
- Elegir bien el tiempo de revalidación
El valor de revalidate no se elige al azar. La pregunta correcta es: ¿cuántos segundos de desfase entre la realidad y lo que ve el usuario son aceptables aquí?
| Contenido de CicloUrbano | Valor razonable | Razonamiento |
|---|---|---|
| Condiciones del servicio, «sobre nosotros» | false (nunca caduca) |
Cambia con un despliegue; revalidar bajo demanda si hace falta |
| Tarifas | 86400 (1 día) |
Un cambio de precio se comunica; un día de desfase es tolerable |
| Listado de estaciones (datos fijos) | 3600 (1 hora) |
Nombre, barrio y plazas totales apenas cambian |
| Ficha de bicicleta | 60 (1 minuto) |
Buen equilibrio entre frescura y coste, con etiqueta para forzar |
| Plazas libres por estación | 60 (1 minuto) |
Orientativo por definición; nadie espera exactitud al segundo |
| Catálogo con disponibilidad en vivo | 0 / SSR |
Aquí la obsolescencia sí engaña al usuario |
| Panel de taller | No aplica | Datos privados; va en la SPA |
Dos heurísticas útiles:
- Empieza generoso y baja si hace falta. Un valor alto con revalidación bajo demanda como salida de emergencia suele ser mejor que un valor bajo: cuesta menos y da control exacto.
- Si el número correcto parece ser 0, la ruta no es estática. Fuérzala a dinámica y sé explícito; no simules SSR con
revalidate: 1, porque pagas la complejidad de las dos estrategias sin la ventaja de ninguna.
- Contenido desde ficheros locales o desde un CMS
Un componente de servidor no está limitado a fetch. Como se ejecuta en Node, puede leer el sistema de ficheros, algo impensable en la SPA. Es la forma más directa de gestionar las páginas informativas de CicloUrbano en Markdown.
---
titulo: Condiciones del servicio
descripcion: Condiciones de alquiler de bicicletas de CicloUrbano.
actualizado: 2026-03-14
---
## Facturación
El alquiler se factura por horas completas desde el desbloqueo.// src/app/[pagina]/page.jsx — una ruta para todas las informativas
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 DIRECTORIO = path.join(process.cwd(), 'contenido');
export async function generateStaticParams() {
const ficheros = await fs.readdir(DIRECTORIO);
return ficheros.map((fichero) => ({ pagina: fichero.replace(/\.md$/, '') }));
}
// Conjunto cerrado: cualquier otra URL debe ser un 404.
export const dynamicParams = false;
async function leerPagina(nombre) {
try {
const bruto = await fs.readFile(path.join(DIRECTORIO, `${nombre}.md`), 'utf8');
const { data, content } = matter(bruto);
const procesado = await remark().use(html).process(content);
return { meta: data, html: procesado.toString() };
} catch {
return null;
}
}
export async function generateMetadata({ params }) {
const { pagina } = await params;
const contenido = await leerPagina(pagina);
if (!contenido) return { title: 'Página no encontrada' };
return { title: contenido.meta.titulo, description: contenido.meta.descripcion };
}
export default async function PaginaInformativa({ params }) {
const { pagina } = await params;
const contenido = await leerPagina(pagina);
if (!contenido) notFound();
return (
<article>
<h1>{contenido.meta.titulo}</h1>
<p><small>Actualizado el {contenido.meta.actualizado}</small></p>
<div dangerouslySetInnerHTML={{ __html: contenido.html }} />
</article>
);
}Puntos a destacar:
fsen un componente. Solo funciona porque es de servidor. Si añadieras'use client'a este fichero, la construcción fallaría: no hay sistema de ficheros en el navegador.dynamicParams = falsees lo correcto aquí: el conjunto de páginas informativas está cerrado y una URL inventada debe dar 404.dangerouslySetInnerHTMLes aceptable porque el Markdown lo escribe el equipo, no un usuario anónimo. Con contenido de terceros habría que sanearlo antes.- Sustituir los ficheros por un CMS (Contentful, Strapi, Sanity, WordPress headless) no cambia la estructura: solo cambia
leerPaginapor unfetcha su API, y la revalidación bajo demanda se dispara desde el webhook del CMS alroute.jsdel apartado 10.
- Rutas estáticas especiales:
sitemap, robots y las imágenes de previsualización
sitemap, robots y las imágenes de previsualizaciónNext.js reserva algunos nombres de fichero que generan artefactos estáticos en la construcción. Son los que completan el trabajo de SEO empezado en 10-01.
El mapa del sitio. Un fichero que exporta una función por defecto y produce sitemap.xml:
// src/app/sitemap.js
const BASE = 'https://ciclourbano.test';
export default async function sitemap() {
const [bicicletas, estaciones] = await Promise.all([
fetch('http://localhost:3001/bicicletas').then((r) => r.json()),
fetch('http://localhost:3001/estaciones').then((r) => r.json()),
]);
const fijas = ['', '/estaciones', '/condiciones', '/tarifas'].map((ruta) => ({
url: `${BASE}${ruta}`,
lastModified: new Date(),
changeFrequency: ruta === '' ? 'hourly' : 'monthly',
priority: ruta === '' ? 1 : 0.6,
}));
const fichas = bicicletas.map((bicicleta) => ({
url: `${BASE}/bicicletas/${bicicleta.id}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.8,
}));
const paradas = estaciones.map((estacion) => ({
url: `${BASE}/estaciones/${estacion.id}`,
changeFrequency: 'weekly',
priority: 0.7,
}));
return [...fijas, ...fichas, ...paradas];
}Las reglas para robots. Aquí se hace explícito el reparto de la arquitectura:
// src/app/robots.js
export default function robots() {
return {
rules: [
{
userAgent: '*',
allow: '/',
// Lo que vive en la SPA o es privado no debe rastrearse.
disallow: ['/api/', '/taller', '/reservas'],
},
],
sitemap: 'https://ciclourbano.test/sitemap.xml',
};
}Las imágenes de previsualización abierta, generadas en la construcción. En 10-01 apuntábamos images: [{ url: '/imagenes/bici-002.jpg' }] a un fichero que alguien tenía que crear a mano. Next.js puede dibujarla con 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 ImagenPrevisualizacion({ 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.modelo}</div>
<div style={{ fontSize: 40 }}>
{bicicleta.precioHora.toFixed(2)} €/h · {bicicleta.tipo}
</div>
</div>
),
size,
);
}Notas: el JSX de ImageResponse no es React del navegador —es una descripción que se convierte en PNG—, y por eso solo admite un subconjunto de CSS basado en Flexbox, con display: 'flex' obligatorio en los contenedores. Como convive con generateStaticParams, las imágenes se generan en la construcción y se sirven desde el CDN, con lo que la tarjeta de WhatsApp de cada bicicleta es distinta sin trabajo de diseño por ficha.
- Imágenes y fuentes:
next/image y next/font
next/image y next/fontDos optimizaciones que el escaparate aprovecha directamente y que no existen en el proyecto Vite sin instalar nada.
next/image sustituye a <img> y resuelve de una vez varios problemas de rendimiento:
import Image from 'next/image';
<Image
src={`/imagenes/${bicicleta.id}.jpg`}
alt={`Bicicleta ${bicicleta.modelo} de CicloUrbano`}
width={640}
height={480}
priority={esLaPrincipal}
sizes="(max-width: 768px) 100vw, 640px"
/>| Qué hace | Por qué importa |
|---|---|
| Convierte a WebP/AVIF y redimensiona bajo demanda | Reduce el peso de la imagen de forma drástica |
Genera srcset a partir de sizes |
El móvil no descarga la versión de escritorio |
| Carga perezosa por defecto | Las tarjetas de abajo no compiten con las de arriba |
Reserva el espacio con width/height |
Elimina el salto de contenido (CLS) |
priority desactiva la carga perezosa y precarga |
Mejora el LCP en la imagen principal |
alt es obligatorio; la accesibilidad del módulo 3 sigue aplicando igual.
next/font autoaloja las fuentes en la construcción, sin peticiones a servidores externos:
// src/app/layout.jsx (fragmento)
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--fuente-base',
});
export default function PlantillaRaiz({ children }) {
return (
<html lang="es" className={inter.variable}>
<body>{children}</body>
</html>
);
}Esto descarga la fuente en next build, la sirve desde tu propio dominio (mejor privacidad y una conexión menos), y genera los descriptores necesarios para que el cambio de fuente no mueva el texto, que es otra fuente clásica de CLS.
- Cuándo no conviene el estático
El estático es tan cómodo que es fácil pasarse. Estos son los límites reales:
| Situación | Problema | Qué hacer |
|---|---|---|
| 200.000 páginas que cambian a diario | La construcción tarda horas y se repite en cada despliegue | generateStaticParams con las más visitadas + dynamicParams: true |
| Contenido personalizado por usuario | Un HTML por usuario no tiene sentido | SSR con cookies(), o CSR dentro de una isla de cliente |
| Datos que deben ser exactos al segundo | El ISR sirve por definición contenido obsoleto | SSR sin caché |
| Búsqueda con parámetros libres | Infinitas combinaciones de searchParams |
Ruta dinámica, o filtrar en el cliente |
| Precios, existencias, saldos | Mostrar un dato caducado tiene consecuencias legales o económicas | SSR, o marcar la parte crítica como dinámica |
| Contenido tras autenticación | No se puede prerenderizar lo que depende de una sesión | SPA o SSR |
Hay un patrón que resuelve la mayoría de estos casos sin renunciar al estático: una página estática con un hueco dinámico. La ficha de bici-002 se prerenderiza entera —modelo, precio, descripción, imagen— y solo el bloque de disponibilidad en vivo se rellena aparte, envuelto en <Suspense>. Se obtiene el TTFB del CDN y la exactitud del SSR en la misma página. Ese patrón es el corazón de 10-03, y es la razón de que la próxima lección sea sobre el modelo y no sobre más funciones de Next.js.
- Tabla resumen definitiva y arquitectura híbrida
Cierre del mapa. Esta es la tabla de las cuatro estrategias aplicada pantalla por pantalla a CicloUrbano, con la implementación concreta:
| Pantalla | Estrategia | Proyecto | Implementación |
|---|---|---|---|
/ catálogo con disponibilidad |
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 por fetch, dos ritmos |
/estaciones/[estacionId] (+pestañas) |
ISR 5 min | ciclourbano-web |
generateStaticParams en el layout |
/condiciones, /tarifas, /sobre-nosotros |
SSG | ciclourbano-web |
Markdown local + dynamicParams: false |
sitemap.xml, robots.txt, imágenes OG |
SSG | ciclourbano-web |
sitemap.js, robots.js, opengraph-image.jsx |
/acceso |
CSR | SPA Vite | PaginaAcceso + sliceSesion |
/reservas, /reservas/nueva |
CSR | SPA Vite | TanStack Query + sliceReservas |
/taller |
CSR | SPA Vite | RutaProtegida + RequiereRol |
Y así queda la arquitectura completa:
flowchart TB
subgraph PUBLICO["Escaparate publico · ciclourbano-web (Next.js 15)"]
CDN["CDN"]
SSG["SSG: condiciones, tarifas, sitemap, robots"]
ISR["ISR: fichas de bicicleta, estaciones"]
SSR["SSR: catalogo con disponibilidad"]
end
subgraph GESTION["Aplicacion de gestion · SPA (Vite + React Router)"]
SPA["CSR: acceso, reservas, 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["Visitante anonimo<br/>y buscadores"]
SSR --> U1
SPA --> U2["Usuario identificado<br/>Ana Ribera / Marc Sole"]
U1 -. "Iniciar sesion" .-> SPA
Esta arquitectura es habitual en producción y merece nombrarse: el escaparate y la aplicación son dos proyectos, dos despliegues y dos modelos de renderizado, unidos por una API común y por una cookie de sesión compartida. No es una transición a medias hacia Next.js: es la decisión correcta, porque las dos partes tienen requisitos opuestos. Lo público necesita ser rápido, indexable y resistente; lo privado necesita ser interactivo y estar siempre al día.
Sobre el proyecto final. Conviene decirlo aquí con claridad para que no haya confusión: el Módulo 11 construye la aplicación completa con Vite + React Router, la que llevas nueve módulos montando. Next.js es una herramienta que ahora sabes usar y situar, no un cambio de rumbo del curso. Cuando en un proyecto real tengas que decidir, tendrás el criterio de la tabla de arriba.
Errores Comunes y Consejos
- Probar el ISR con
npm run dev. En desarrollo, Next.js renderiza siempre y no cachea nada. El ISR solo se comprueba connpm run build && npm run start. - Esperar que
revalidate: 60regenere sola cada minuto. No hay temporizador: la regeneración la dispara una visita después de la caducidad. Sin tráfico, no hay regeneración. - Añadir
console.logpara depurar y encontrarlo vacío. Si la página es estática, eseconsole.logse ejecutó una vez, durantenext build, y su salida está en el registro de la construcción, no en el del servidor. - Devolver claves con el nombre equivocado en
generateStaticParams. Si la carpeta es[bicicletaId], la clave debe serbicicletaId. Y el valor, una cadena. - Usar
searchParamssin darse cuenta de que rompe el prerenderizado. Una sola lectura convierte la página enƒ. Si necesitas parámetros de consulta sin perder el estático, léelos en cliente conuseSearchParams. - Dejar
/api/revalidarsin autenticación. Es un vector de denegación de servicio: cualquiera puede forzar renders en bucle. Secreto en cabecera, siempre. - Fijar
revalidate: 1«por si acaso». Es SSR con complejidad extra. Si necesitas frescura absoluta, declara la ruta dinámica y sé explícito. - Olvidar
widthyheightennext/image. Sin ellos no se reserva espacio y vuelve el salto de contenido que el componente venía a evitar. - Consejo: haz de
next buildparte de tu integración continua. Combinado conexport const dynamic = 'error'en las rutas que deben ser estáticas, unfetchmal configurado deja de ser un problema de producción y pasa a ser un fallo de construcción. - Consejo: prerenderiza solo lo que se visita. En catálogos grandes,
generateStaticParamscon lo más popular ydynamicParams: trueda el 95 % del beneficio con una fracción del tiempo de construcción.
Ejercicios
Ejercicio 1. Este next build no es el esperado. Las tres primeras rutas deberían ser estáticas o SSG y aparecen como dinámicas. Para cada una, propón la causa más probable y la corrección.
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
Sabes además que: /condiciones solo contiene texto en JSX; /bicicletas/[bicicletaId] tiene generateStaticParams; /estaciones hace dos fetch sin opciones; y / lee searchParams. Indica también cuál de las cuatro debe seguir siendo ƒ.
Ejercicio 2. Diseña la estrategia de caché de /estaciones/[estacionId], que muestra: nombre, barrio y plazas totales (fijos); la flota aparcada ahora mismo (cambia cada pocos minutos); y las incidencias abiertas (deben verse en cuanto un operario da una de alta). Escribe el código de las peticiones con sus revalidate y sus tags, y explica cómo se fuerza la actualización de las incidencias sin esperar.
Ejercicio 3. El equipo de CicloUrbano quiere una sección de blog en el escaparate: artículos en Markdown en contenido/blog/, listado en /blog y detalle en /blog/[slug]. Los artículos se publican una o dos veces al mes. Decide la estrategia de renderizado, el valor de dynamicParams y qué hay que añadir al sitemap.js, y justifica cada decisión en una frase.
Soluciones
Solución 1.
| Ruta | Causa probable | Corrección |
|---|---|---|
/condiciones |
Algo global la contamina: casi seguro un await cookies() en layout.jsx (el MenuUsuario de 10-01), o un export const dynamic = 'force-dynamic' heredado |
Sacar la lectura de la cookie del layout de servidor: leerla en un componente de cliente o aislarla en un componente envuelto en <Suspense> |
/bicicletas/[bicicletaId] |
Tiene generateStaticParams, pero los fetch van sin caché: en Next.js 15 eso basta para hacerla dinámica |
Cambiar a next: { revalidate: 60, tags: [...] } |
/estaciones |
Mismo motivo: fetch sin opciones ya no se cachea en Next.js 15 |
Añadir next: { revalidate: N } a cada petición |
/ |
Lee searchParams y muestra disponibilidad en vivo |
Debe seguir siendo ƒ. Es el caso legítimo de SSR de todo el escaparate |
El aprendizaje general: una ruta que parece estática y sale como ƒ casi siempre tiene la causa en un ancestro o en una opción de fetch olvidada.
Solución 2.
// src/app/estaciones/[estacionId]/page.jsx
const API = 'http://localhost:3001';
export async function generateStaticParams() {
const estaciones = await fetch(`${API}/estaciones`).then((r) => r.json());
return estaciones.map((estacion) => ({ estacionId: estacion.id }));
}
export default async function PestanaFlota({ params }) {
const { estacionId } = await params;
const [estacion, flota, incidencias] = await Promise.all([
// 1. Datos fijos: una hora es más que suficiente.
fetch(`${API}/estaciones/${estacionId}`, {
next: { revalidate: 3600, tags: [`estacion-${estacionId}`] },
}).then((r) => r.json()),
// 2. Flota aparcada: cambia cada pocos minutos.
fetch(`${API}/bicicletas?estacionId=${estacionId}`, {
next: { revalidate: 120, tags: ['bicicletas', `flota-${estacionId}`] },
}).then((r) => r.json()),
// 3. Incidencias: caducidad larga porque se invalidan bajo demanda.
fetch(`${API}/incidencias?estacionId=${estacionId}`, {
next: { revalidate: 3600, tags: [`incidencias-${estacionId}`] },
}).then((r) => r.json()),
]);
return (
<>
<p>{estacion.barrio} · {estacion.plazas} plazas</p>
<p>{flota.length} bicicletas aparcadas</p>
<p>{incidencias.length} incidencias abiertas</p>
</>
);
}La clave está en la tercera petición: no se usa un revalidate corto, sino uno largo con una etiqueta. Cuando un operario da de alta una incidencia desde el panel de taller, el sistema de gestión llama al endpoint de revalidación:
curl -X POST http://localhost:3000/api/revalidar \
-H "x-secreto-revalidacion: $SECRETO_REVALIDACION" \
-H "Content-Type: application/json" \
-d '{"etiqueta":"incidencias-est-02"}'Así se obtiene lo mejor de las dos estrategias: coste mínimo en régimen normal (una regeneración por hora como mucho) y actualización inmediata cuando de verdad hay algo nuevo. Bajar revalidate a 10 segundos daría un resultado peor y más caro.
Solución 3.
- Estrategia: SSG puro para el listado y para el detalle. Los artículos son ficheros del repositorio, así que cualquier cambio implica un despliegue, y el despliegue ya reconstruye el sitio: una revalidación por tiempo no aportaría nada.
dynamicParams = falseen/blog/[slug]: el conjunto de artículos está cerrado en el momento de la construcción, y un slug inventado debe devolver un 404 real en lugar de disparar un render.generateStaticParamsleecontenido/blog/confs.readdiry devuelve un{ slug }por fichero.- En
sitemap.jshay que añadir la ruta/blogy una entrada por artículo, conlastModifiedtomado del campofechadel front matter —no denew Date(), que marcaría todos los artículos como recién modificados en cada construcción y degradaría la señal para los buscadores— ychangeFrequency: 'monthly'. - Extra recomendable: un
opengraph-image.jsxen/blog/[slug]con el título del artículo, para que compartir un enlace en un chat muestre una tarjeta propia.
Si en el futuro los artículos se movieran a un CMS, la estrategia pasaría a ISR con revalidación bajo demanda desde el webhook del CMS: el resto del código no cambiaría.
Conclusión
Con esta lección queda completo el mapa de renderizado que abría el módulo. En 10-01 el HTML se generaba en cada petición; aquí se genera una vez, en next build, se sirve desde un CDN y —con ISR— se refresca solo cuando hace falta.
Lo esencial de la estrategia. En el App Router toda ruta es estática por defecto y se vuelve dinámica en cuanto usa algo que solo se conoce en la petición: cookies(), headers(), searchParams o un fetch sin caché —que en Next.js 15 es el comportamiento por defecto, así que la caché hay que pedirla—. Y no se supone nada: next build lo dice, con ○ para estático, ● para SSG con generateStaticParams y ƒ para dinámico. Ese comando pertenece a la rutina previa al despliegue tanto como npm test.
De las herramientas, quedan fijadas cuatro. generateStaticParams enumera los valores de un segmento dinámico y genera una página por cada uno, con la clave nombrada exactamente como la carpeta. dynamicParams decide qué pasa con lo que no estaba: true lo renderiza bajo demanda y lo cachea —lo correcto para un catálogo que crece—, false devuelve un 404 —lo correcto para un conjunto cerrado—. revalidate, por ruta o por petición, implementa el patrón stale-while-revalidate: nadie espera nunca, alguien ve contenido ligeramente antiguo, y la caducidad no es un temporizador sino una condición que dispara la siguiente visita. Y revalidateTag / revalidatePath invierten el control para los casos en que esperar no vale: etiquetas declaradas en el fetch, un route.js autenticado y una llamada desde el sistema de gestión. La combinación ganadora, que has visto en el ejercicio 2, es caducidad larga más etiqueta: coste mínimo en reposo y actualización inmediata cuando hay novedad.
El escaparate ha ganado además su capa de SEO y rendimiento completa: sitemap.js con las fichas y las estaciones, robots.js que excluye lo que vive en la SPA, opengraph-image.jsx que dibuja una tarjeta distinta por bicicleta en la construcción, next/image con su srcset, su carga perezosa y su reserva de espacio, y next/font autoalojando la tipografía sin saltos de texto. Y las páginas informativas salen de ficheros Markdown leídos con fs desde un componente de servidor, algo que en la SPA era sencillamente imposible.
La arquitectura de CicloUrbano queda cerrada y es deliberadamente híbrida: escaparate público en ciclourbano-web —catálogo en SSR, fichas y estaciones en ISR, informativas en SSG— y aplicación de gestión en la SPA de Vite —acceso, reservas y taller en CSR con Redux Toolkit y TanStack Query—, unidas por la API común y por una cookie de sesión compartida. Y con ello, el recordatorio que ya se ha hecho arriba: el proyecto final del Módulo 11 se construye con Vite + React Router.
Queda una pieza sin explicar, y es justamente la que sostiene todo lo anterior. Has escrito 'use client' sin saber del todo qué significa; has aceptado que un componente de servidor «no se envía al navegador» sin ver cómo; has puesto un loading.jsx sabiendo que por debajo hay un <Suspense>; y el apartado 15 ha terminado prometiendo el patrón que resuelve casi todos los casos difíciles: una página estática con un hueco dinámico dentro. La próxima lección deja de lado la sintaxis de Next.js y va al modelo: qué es exactamente un límite de suspensión, cómo un componente «se suspende», cómo el servidor envía el HTML por partes, dónde se ejecuta cada componente y qué puede y no puede cruzar la frontera entre servidor y cliente. La próxima lección es Suspense y React Server Components.
Curso de React
Módulo 1: Introducción a React
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
