CicloUrbano funciona y está probada, pero vive en localhost, con una base de datos que es un fichero JSON y un servidor de desarrollo que jamás debería asomarse a internet. Esta lección cubre el último tramo: convertir el proyecto en un paquete de ficheros estáticos, entender qué se puede y qué no se puede esconder en una aplicación que se ejecuta en el navegador de otra persona, resolver el problema de enrutamiento que hace que /reservas devuelva un 404 en cuanto sale de localhost, desplegarlo, y saber qué vigilar después. Y como es la última lección del curso, cierra también el camino recorrido y señala por dónde seguir.
Contenido
- La construcción para producción
- Revisar el tamaño antes de publicar
- Variables de entorno: qué es realmente público
- El 404 al recargar: enrutamiento en cliente sobre un servidor estático
- Despliegue paso a paso en una plataforma estática
- La alternativa del contenedor: Docker y Nginx
- La API en producción: qué falta de verdad
- Después del despliegue: monitorización y métricas reales
- Lista de comprobación de lanzamiento
- Siguientes pasos con este proyecto
- Cómo seguir aprendiendo
- La construcción para producción
Hasta ahora todo ha ocurrido con npm run dev, donde Vite sirve los módulos sin empaquetar y recarga en caliente. Eso es cómodo para desarrollar y no es lo que se publica. La construcción es otra cosa:
vite v6.0.5 building for production... ✓ 412 modules transformed. dist/index.html 0.48 kB │ gzip: 0.31 kB dist/assets/index-B7dK2p9x.css 14.82 kB │ gzip: 3.41 kB dist/assets/PaginaTaller-Qm3rT8vc.js 6.12 kB │ gzip: 2.18 kB dist/assets/PaginaDetalleEstacion-Nx7.js 8.94 kB │ gzip: 3.02 kB dist/assets/PaginaNuevaReserva-Zk1p.js 11.37 kB │ gzip: 3.86 kB dist/assets/index-Yt4mR9Ka.js 268.41 kB │ gzip: 87.65 kB ✓ built in 3.42s
Qué ha hecho, paso a paso:
| Tarea | Qué implica |
|---|---|
| Empaquetado | Cientos de módulos se combinan en unos pocos ficheros, para no hacer cientos de peticiones |
| Minificación | Se eliminan espacios, comentarios y nombres largos de variables locales |
| Eliminación de código muerto | Lo que no se importa desde ninguna parte no se incluye |
| División en fragmentos | Cada ruta con lazy produce su propio fichero, como se preparó en División de Código |
| Modo producción de React | Desaparecen los avisos de desarrollo, las comprobaciones extra y el doble render de StrictMode |
| Hash en el nombre | index-Yt4mR9Ka.js contiene una huella del contenido |
Ese hash es la pieza que más se malinterpreta. Sirve para poder decirle al servidor «guarda estos ficheros en caché para siempre» sin condenar al usuario a una versión antigua: si el contenido cambia, el nombre cambia, y el navegador lo pide como si fuera un fichero nuevo. La consecuencia práctica es una regla de caché de dos velocidades:
| Fichero | Cabecera de caché | Por qué |
|---|---|---|
index.html |
no-cache (o muy corta) |
Es el único que no lleva hash: es quien apunta a los demás |
assets/* con hash |
max-age=31536000, immutable |
Su nombre cambia si cambia el contenido; cachearlo un año es seguro |
Antes de publicar, se verifica la construcción tal cual, no el servidor de desarrollo:
Es el momento de comprobar tres cosas que solo se ven aquí: que no hay errores en la consola sin los avisos de desarrollo, que los fragmentos perezosos se descargan al navegar (mira la pestaña de red) y que el Suspense de cada ruta muestra su esqueleto y no un salto en blanco.
- Revisar el tamaño antes de publicar
El informe de arriba ya da la cifra que importa: 87,65 kB comprimidos para el paquete principal. La revisión útil no es «¿es mucho?» en abstracto, sino comparar con el presupuesto que se fijó y localizar quién ocupa qué:
Un desglose típico de este proyecto:
| Dependencia | Aproximado (gzip) | ¿Se puede reducir? |
|---|---|---|
react + react-dom |
~45 kB | No: es el motor |
react-router |
~12 kB | No, y ya divide por rutas |
@reduxjs/toolkit + react-redux |
~15 kB | Solo eliminando Redux, que aquí se justificó |
@tanstack/react-query |
~13 kB | No: sustituye a mucho código propio |
| Código de CicloUrbano | ~3 kB | Ya está dividido por rutas |
La lección: en una aplicación bien dividida, el peso es casi todo dependencias, y la palanca que queda no es minificar mejor, sino elegir menos bibliotecas. Si el presupuesto se pasa, la conversación es sobre arquitectura, no sobre configuración.
- Variables de entorno: qué es realmente público
Vite expone al código solo las variables que empiezan por VITE_, y las sustituye en la construcción:
# .env.development
VITE_API_URL=http://localhost:3001
VITE_ENTORNO=desarrollo
# .env.production
VITE_API_URL=https://api.ciclourbano.example
VITE_ENTORNO=produccion// src/configuracion.js — un único punto de lectura, como se decidió en 11-01
export const configuracion = {
apiUrl: import.meta.env.VITE_API_URL,
entorno: import.meta.env.VITE_ENTORNO,
esProduccion: import.meta.env.PROD,
};Y ahora la advertencia más importante de esta lección, porque es un error que se comete con frecuencia y tiene consecuencias reales:
Todo lo que pongas en
import.meta.envacaba escrito, en texto legible, dentro de los ficheros JavaScript que descarga cualquier persona que visite la web. No es una variable «de servidor»: es una constante que se incrusta en el paquete. Cualquiera puede abrir las herramientas del navegador y leerla.
# Compruébalo tú mismo tras construir:
grep -r "api.ciclourbano.example" dist/assets/
# → aparece, tal cual, en el paqueteDe ahí se deriva una frontera clara:
Puede ir en VITE_* |
No puede ir nunca |
|---|---|
| URL pública de la API | Claves de API con permisos de escritura |
| Identificador público de un servicio de analítica | Secretos de cliente de OAuth |
| Nombre del entorno, versión, banderas de funcionalidad | Credenciales de base de datos |
| Clave publicable de una pasarela de pago | Clave secreta de la misma pasarela |
Lo que necesite un secreto necesita un servidor que lo guarde y haga la llamada por ti. No hay atajo. Y .env.production no se versiona: en el repositorio va .env.example con las claves y sin los valores, y los valores reales se configuran en la plataforma de despliegue.
- El 404 al recargar: enrutamiento en cliente sobre un servidor estático
Este es el fallo que sorprende a todo el mundo en su primer despliegue de una aplicación de una sola página. En local funciona; desplegado, entrar directamente en https://ciclourbano.example/reservas devuelve un 404.
La razón es que hay dos entidades distintas resolviendo rutas y solo una de ellas conoce las tuyas:
sequenceDiagram
participant N as Navegador
participant S as Servidor estático
participant R as React Router
Note over N,R: Navegación interna: funciona
N->>R: clic en «Mis reservas»
R->>N: pinta PaginaReservas y cambia la URL
Note over N,R: Recarga o enlace directo: 404
N->>S: GET /reservas
S->>S: ¿existe el fichero /reservas?
S--xN: 404 · React Router nunca llegó a ejecutarse
React Router vive dentro del JavaScript que carga index.html. Si el servidor no entrega index.html, no hay React, no hay Router y no hay ruta. La solución es siempre la misma idea —devolver index.html para cualquier ruta que no sea un fichero real— con una sintaxis distinta en cada plataforma:
| Plataforma | Fichero | Contenido |
|---|---|---|
| Netlify | public/_redirects |
/* /index.html 200 |
| Vercel | vercel.json |
Ver abajo |
| GitHub Pages | public/404.html |
Una copia de index.html (no admite reescrituras) |
| Nginx | Bloque server |
try_files $uri $uri/ /index.html; |
| Apache | public/.htaccess |
Ver abajo |
| Cloudflare Pages | public/_redirects |
Igual que Netlify |
// vercel.json
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
"headers": [
{
"source": "/assets/(.*)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
}
]
}# public/.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Si el fichero o el directorio existe, sírvelo tal cual
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Cualquier otra cosa: entrega index.html y deja que React Router decida
RewriteRule ^ index.html [L]
</IfModule>Fíjate en el detalle del código 200 en Netlify: es una reescritura, no una redirección. La URL que ve el usuario sigue siendo /reservas, que es justo lo que React Router necesita leer. Con un 301 o un 302 la URL cambiaría a / y el usuario acabaría en el catálogo.
Y la comprobación que nunca hay que saltarse tras desplegar: abrir una ruta profunda en una pestaña nueva y recargarla. Navegar hasta ella desde el inicio no prueba nada, porque ese camino nunca toca el servidor.
- Despliegue paso a paso en una plataforma estática
El proyecto son ficheros estáticos, así que casi cualquier alojamiento sirve. Con una plataforma conectada al repositorio, el despliegue queda automatizado:
Paso 1. Sube el proyecto a un repositorio remoto (GitHub, GitLab). El flujo de trabajo de CI escrito en la lección anterior empieza a ejecutarse en cada cambio.
Paso 2. Conecta el repositorio en la plataforma y configura la construcción:
| Ajuste | Valor |
|---|---|
| Comando de construcción | npm run build |
| Directorio de publicación | dist |
| Versión de Node | 20 (la misma que en CI, para no descubrir diferencias tarde) |
| Comando de instalación | npm ci |
Paso 3. Añade las variables de entorno en el panel de la plataforma: VITE_API_URL con la URL real de la API y VITE_ENTORNO=produccion. Nunca en el repositorio.
Paso 4. Añade el fichero de reescritura de la sección anterior. Sin él, el despliegue «funciona» hasta que alguien comparte un enlace.
Paso 5. Despliega y comprueba, en este orden: la portada carga; una ruta profunda recargada en pestaña nueva funciona; el catálogo trae datos de la API real; una ruta inexistente muestra tu página de no encontrado; el panel de taller sigue vedado a un cliente; y la consola está limpia.
Paso 6. Activa los despliegues de vista previa por rama. Cada rama y cada solicitud de cambios obtiene su propia URL temporal, así que las revisiones dejan de ser «confía en mí» y pasan a ser «pruébalo aquí». Es, con diferencia, la función que más cambia la forma de trabajar en equipo.
- La alternativa del contenedor: Docker y Nginx
Si el despliegue va a la infraestructura propia de la empresa, la forma equivalente es una imagen que construye y sirve:
# Dockerfile
# --- Etapa 1: construir ---
FROM node:20-alpine AS construccion
WORKDIR /app
# Copiar primero los manifiestos aprovecha la caché de capas:
# si no cambian, no se reinstalan las dependencias
COPY package*.json ./
RUN npm ci
COPY . .
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
# --- Etapa 2: servir ---
# La imagen final no lleva Node ni node_modules: solo ficheros estáticos y Nginx
FROM nginx:alpine AS servicio
COPY --from=construccion /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# nginx.conf
server {
listen 80;
root /usr/share/nginx/html;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
# Los ficheros con hash se pueden cachear indefinidamente
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# index.html nunca se cachea: es quien apunta a los assets nuevos
location = /index.html {
add_header Cache-Control "no-cache";
}
# La pieza clave: cualquier ruta desconocida entrega index.html
location / {
try_files $uri $uri/ /index.html;
}
}Dos detalles que conviene entender. El primero es que la construcción de dos etapas deja una imagen final de unos pocos megabytes: Node solo existe mientras se construye. El segundo es el ARG VITE_API_URL: como las variables se incrustan en la construcción, una imagen construida apunta a una API concreta. No puedes cambiarla al arrancar el contenedor. Si necesitas una imagen que sirva a varios entornos, la URL debe leerse en tiempo de ejecución (por ejemplo, de un /configuracion.json que se pide al arrancar la aplicación).
- La API en producción: qué falta de verdad
Hay que decirlo sin adornos: json-server es una herramienta de desarrollo. Acepta cualquier escritura de cualquiera, no tiene autenticación, no valida nada y guarda los datos en un fichero. Desplegarlo con datos reales sería un incidente de seguridad, no un despliegue.
Lo que hace falta detrás de una aplicación como esta:
| Pieza | Por qué | Qué implica |
|---|---|---|
| API real | Persistencia fiable, transacciones, integridad | Un servicio propio (Node, Python, Java…) o una plataforma tipo backend as a service |
| Autenticación | Saber quién pide algo | Tokens de sesión en cookies httpOnly + Secure + SameSite; renovación y caducidad |
| Autorización en el servidor | Que /taller sea de verdad solo del operario |
Comprobación de rol en cada endpoint, no solo en la interfaz |
| Validación en el servidor | validarReserva es una comodidad, no una defensa |
Repetir las reglas en el servidor; es la única copia que cuenta |
| CORS | Permitir a tu dominio y solo a él | Cabeceras con la lista de orígenes permitidos |
| Límites de tasa | Evitar abuso y coste descontrolado | Cortafuegos de aplicación o límites por IP y por usuario |
| Bloqueo de sobrerreserva | Dos personas reservando bici-001 a la vez |
Unicidad y transacción en la base de datos |
Vale la pena releer con esta luz lo que se dijo en Rutas Protegidas: RutaProtegida y RequiereRol mejoran la experiencia y no protegen nada. Cualquiera puede abrir las herramientas del navegador, modificar el estado de la sesión y ver la pantalla del taller. Lo que impide que haga daño no es tu componente: es el servidor rechazando la petición.
Un último aviso de sensatez profesional: en cuanto haya datos de personas reales de por medio —nombres, correos, ubicaciones, pagos— el diseño de autenticación, almacenamiento y permisos debe revisarlo alguien con experiencia en seguridad y protección de datos antes de exponerlo al público. Todo el dominio de este curso es ficticio precisamente para poder aprender sin ese riesgo.
- Después del despliegue: monitorización y métricas reales
Publicar no es terminar. En producción hay navegadores, redes y personas que no estaban en tu portátil, y hay dos preguntas que solo se responden con datos de allí.
¿Está fallando algo? El registrarError de src/utilidades/monitorizacion.js lleva todo el curso siendo un console.error. Ahora es cuando conecta con un servicio de verdad:
// src/utilidades/monitorizacion.js
import { configuracion } from '../configuracion.js';
export function registrarError(error, pilaComponentes, contexto = {}) {
if (!configuracion.esProduccion) {
console.error('[CicloUrbano]', error, pilaComponentes, contexto);
return;
}
// En producción se envía al servicio de monitorización.
// Nunca incluyas datos personales en el informe: identificadores, no nombres ni correos.
enviarInforme({
mensaje: error.message,
pila: error.stack,
pilaComponentes,
ruta: window.location.pathname,
version: configuracion.version,
usuarioId: contexto.usuarioId ?? null,
});
}Ese enganche ya está puesto donde tenía que estar: el componentDidCatch del LimiteDeError que se escribió en Límites de Error. El valor de haberlo previsto entonces se cobra ahora.
¿Va rápida para la gente de verdad? Las mediciones del Profiler se hicieron en un portátil con buena red. Las métricas de usuarios reales se recogen en el propio navegador y se envían:
// src/main.jsx
if (configuracion.esProduccion) {
import('web-vitals').then(({ onLCP, onINP, onCLS }) => {
onLCP(enviarMetrica); // Cuánto tarda en pintarse lo principal
onINP(enviarMetrica); // Cuánto tarda en responder a una interacción
onCLS(enviarMetrica); // Cuánto se mueve el contenido mientras carga
});
}Y hace falta una tercera cosa que no es técnica: un canal de vuelta. Un enlace visible para informar de un problema, y alguien que lo lea. La mayoría de los fallos que importan los cuenta una persona antes de que ninguna métrica los delate.
- Lista de comprobación de lanzamiento
| Área | Comprobación |
|---|---|
| Construcción | npm run build sin avisos · npm run preview recorrido completo · tamaño dentro del presupuesto |
| Enrutamiento | Ruta profunda recargada en pestaña nueva · ruta inexistente muestra tu 404 · /taller vedado a un cliente |
| Rendimiento | LCP y INP medidos en la construcción de producción · fragmentos perezosos descargándose al navegar |
| Accesibilidad | Recorrido completo con Tab · foco visible · formularios etiquetados · contraste suficiente en ambos temas · auditoría del navegador |
| SEO básico | <title> y descripción · idioma en <html lang="es"> · favicon · robots.txt |
| Errores | registrarError conectado · LimiteDeError con un mensaje humano · ningún dato personal en los informes |
| Seguridad | Ningún secreto en el paquete (grep sobre dist/) · HTTPS · cabeceras de seguridad · autorización comprobada en el servidor |
| Datos | Copias de seguridad de la base de datos · plan de restauración probado |
| Operación | Analítica respetuosa con la privacidad · alertas configuradas · plan de reversión al despliegue anterior |
| Personas | Canal para informar de fallos · alguien de guardia el día del lanzamiento |
El punto que más se olvida y más duele es el plan de reversión. Antes de publicar, hay que saber cómo se vuelve a la versión anterior y cuánto tarda. Las plataformas conectadas al repositorio lo dan hecho con un botón; con contenedores, es volver a desplegar la etiqueta anterior. Averígualo antes de necesitarlo.
- Siguientes pasos con este proyecto
CicloUrbano está viva y tiene una hoja de ruta natural, toda ella apoyada en lo que ya sabes:
| Mejora | Con qué del curso | Qué gana |
|---|---|---|
| Migrar a TypeScript | TypeScript con React, fichero a fichero empezando por src/tipos/dominio.ts |
Contratos explícitos y refactorizaciones seguras; menos pruebas triviales |
| Escaparate público | SSR y SSG con Next.js para catálogo y fichas | Contenido indexable y primer pintado inmediato, junto a la gestión en la SPA |
| Aplicación móvil | React Native con Expo, reutilizando validaciones, hooks y slices | Presencia nativa con la mayor parte de la lógica compartida |
| Internacionalización | Extraer los textos y añadir catalán e inglés | Alcance real de la aplicación |
| Modo sin conexión | Un service worker y la persistencia de la caché de Query | Una app usable en la calle con mala cobertura |
| Pagos y facturación | Una pasarela real, con la clave secreta en el servidor | El paso de prototipo a producto |
| Panel de métricas | El componente de gráficas ya aislado en un fragmento perezoso | Decisiones de negocio con datos |
El mejor consejo para elegir es el mismo que ordenó el proyecto desde el principio: incrementos verticales. Una mejora completa, de la interfaz a la prueba, antes de empezar la siguiente.
- Cómo seguir aprendiendo
- La documentación oficial de React es, hoy, excelente. Sus páginas sobre «no necesitas un efecto» y sobre cómo estructurar el estado explican mejor que ningún tutorial por qué las cosas son como son. Vuelve a ellas con la experiencia que ya tienes: se leen distintas.
- Lee el código de las bibliotecas que usas. React Router, Redux Toolkit y TanStack Query son legibles y están bien escritas. Entender cómo resuelven un problema enseña más que cualquier curso, incluido este.
- Escribe tu propia versión reducida de algo que uses: un
useQuerymínimo con caché, un enrutador de cincuenta líneas. Nada consolida un modelo mental como reimplementarlo. - Reproduce los fallos que te encuentres en un ejemplo mínimo antes de pedir ayuda. La mitad de las veces se resuelven en el intento, y la otra mitad recibes una respuesta mucho mejor.
- Contribuye. Empieza por documentación y por reproducir incidencias; es la puerta de entrada más honesta a un proyecto de código abierto.
- Mantén el escepticismo con lo nuevo. El ecosistema de React se mueve rápido y no todo lo que aparece sobrevive. La forma de distinguir la moda de la mejora es preguntarse qué problema concreto resuelve y si tú lo tienes.
Errores Comunes y Consejos
- Desplegar sin la regla de reescritura. La aplicación parece funcionar hasta que alguien recarga o comparte un enlace. Comprueba siempre una ruta profunda en pestaña nueva.
- Poner un secreto en una variable
VITE_. Queda en texto legible en el paquete. Si algo debe ser secreto, necesita un servidor. - Cachear
index.htmldurante mucho tiempo. Los usuarios se quedan clavados en la versión anterior aunque losassetsnuevos ya estén publicados.index.htmlsin caché,assetscon hash y caché larga. - Confiar en
RutaProtegidacomo seguridad. Es experiencia de usuario. La autorización se comprueba en el servidor, en cada endpoint, siempre. - Probar el rendimiento en el servidor de desarrollo. Los números no se parecen a los de producción. Mide sobre
npm run previewo sobre el despliegue real. - Desplegar un viernes por la tarde sin plan de reversión. No es una broma: si no sabes cómo volver atrás y cuánto tarda, no estás listo para publicar.
- Consejo final: automatiza el despliegue el mismo día que hagas el primero a mano. Un despliegue que requiere recordar pasos es un despliegue que alguien hará mal.
Ejercicios
Ejercicio 1. Despliega CicloUrbano en una plataforma estática con la reescritura correcta y las cabeceras de caché de la sección 1. Documenta en el README.md el procedimiento y el plan de reversión, y verifica con grep sobre dist/ que no se ha colado ningún secreto.
Ejercicio 2. Prepara el proyecto para servirse desde una subruta (https://ejemplo.example/ciclourbano/) en lugar de la raíz del dominio. Averigua qué hay que cambiar en Vite, en React Router y en la configuración del servidor.
Ejercicio 3. Añade al flujo de integración continua un paso que falle la construcción si el paquete principal supera un presupuesto de 100 kB comprimidos, para que el tamaño no se degrade sin que nadie se entere.
Soluciones
Solución 1.
proyecto/ ├── public/_redirects → /* /index.html 200 └── vercel.json (si es Vercel, con rewrites y headers)
# Verificación previa obligatoria: ningún secreto en el paquete
npm run build
grep -rEi "secret|password|sk_live|private_key" dist/assets/ && echo "REVISAR" || echo "limpio"
# Comprobación posterior al despliegue
curl -I https://ciclourbano.example/reservas # debe devolver 200 y text/html
curl -I https://ciclourbano.example/assets/index-*.js # debe traer Cache-Control: immutableEn el README.md:
## Despliegue
Automático al integrar en `main`. La plataforma ejecuta `npm ci` y `npm run build`
y publica `dist/`. Variables necesarias: `VITE_API_URL`, `VITE_ENTORNO`.
### Comprobación tras desplegar
1. Abrir `/reservas` en una pestaña nueva y recargar → debe cargar (no 404).
2. Ruta inexistente → página propia de no encontrado.
3. `/taller` con sesión de cliente → «sin permisos».
4. Consola del navegador sin errores.
### Reversión
Panel de despliegues → seleccionar el anterior correcto → «Restaurar».
Tiempo estimado: menos de 2 minutos. No requiere reconstruir.Solución 2. Hay que tocar tres sitios, y olvidarse de cualquiera de ellos produce una pantalla en blanco o enlaces rotos:
// 1. vite.config.js — prefijo de las rutas de los assets en index.html
export default defineConfig({
base: '/ciclourbano/',
plugins: [react()],
});// 2. src/rutas.jsx — React Router debe ignorar el prefijo al comparar rutas
export const router = createBrowserRouter(rutas, {
basename: '/ciclourbano',
});# 3. Servidor — la reescritura debe apuntar al index.html de la subruta
location /ciclourbano/ {
alias /usr/share/nginx/html/;
try_files $uri $uri/ /ciclourbano/index.html;
}Comprobación: dist/index.html debe referenciar /ciclourbano/assets/..., y una recarga en /ciclourbano/reservas debe funcionar. Los enlaces internos con <Link to="/reservas"> siguen escribiéndose sin el prefijo: lo añade el basename.
Solución 3.
// scripts/comprobar-presupuesto.js
import { readdirSync, readFileSync } from 'node:fs';
import { gzipSync } from 'node:zlib';
import { join } from 'node:path';
const LIMITE_KB = 100;
const CARPETA = 'dist/assets';
// El paquete principal es el que empieza por index- y acaba en .js
const principal = readdirSync(CARPETA).find((f) => /^index-.*\.js$/.test(f));
if (!principal) {
console.error('No se ha encontrado el paquete principal. ¿Se ha ejecutado npm run build?');
process.exit(1);
}
const bytes = gzipSync(readFileSync(join(CARPETA, principal))).length;
const kb = bytes / 1024;
console.log(`${principal}: ${kb.toFixed(2)} kB comprimidos (límite ${LIMITE_KB} kB)`);
if (kb > LIMITE_KB) {
console.error(
`\nPresupuesto superado en ${(kb - LIMITE_KB).toFixed(2)} kB.\n` +
'Revisa las dependencias nuevas con: npx vite-bundle-visualizer'
);
process.exit(1); // El código de salida distinto de cero es lo que rompe la construcción en CI
}# .github/workflows/pruebas.yml — añadido al trabajo de extremo a extremo
- run: npm run build
- run: npm run presupuesto # Falla el trabajo si el paquete ha engordadoLa clave es el process.exit(1): sin él, el script informa pero la integración continua sigue en verde y el presupuesto se convierte en una sugerencia que nadie atiende.
Conclusión
Empezaste este curso con un <div id="root"> vacío y la pregunta de por qué alguien necesitaría una biblioteca para pintar una lista de bicicletas. Acabas con una aplicación completa, probada en cuatro niveles, medida, dividida en fragmentos, accesible y desplegada, y con criterio propio para decidir cómo construir la siguiente.
El camino ha sido este. En los fundamentos entendiste que React es declarativo y que la interfaz es una función del estado, y viste qué ocurre de verdad entre un cambio de estado y un píxel en la pantalla. Con los componentes, las props y el estado aprendiste a delimitar piezas y a parametrizarlas, y con los eventos, los formularios y la accesibilidad hiciste que respondieran a personas, no solo a datos. En los conceptos avanzados apareció la disciplina: elevar el estado al ancestro común, componer en lugar de heredar, y capturar los fallos antes de que se coman la pantalla. Los hooks te dieron el vocabulario completo —estado, sincronización con el exterior, referencias, contexto, reductores— y, sobre todo, la capacidad de extraer tu propia lógica reutilizable. React Router convirtió una pantalla en una aplicación con URLs compartibles y accesos controlados. La gestión del estado puso orden en el caos con una idea que vale más que cualquier biblioteca: cada dato tiene un sitio donde debe vivir, y los datos del servidor no son tuyos. El rendimiento te enseñó a medir antes de tocar y a no confundir optimización con decoración. Las pruebas te dieron la libertad de refactorizar sin miedo, probando comportamiento y no implementación. Los temas avanzados ampliaron el mapa hasta el servidor, los tipos y el móvil. Y el proyecto demostró que todo eso encaja.
Lo que te llevas no es una lista de APIs, que cambiarán. Es la forma de pensar que hay debajo: describir la interfaz en función del estado, poner cada dato en su sitio, medir antes de optimizar, probar el comportamiento visible, y no confiar nunca en el cliente para lo que debe garantizar el servidor. Eso sigue siendo cierto cuando cambia la versión, la biblioteca de moda o el framework entero.
Ya tienes lo que hace falta. Construye algo.
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
