Módulo 01: Entorno de desarrollo

Objetivos del módulo

  • Entender qué papel cumple cada herramienta de un proyecto React actual y por qué hacen falta
  • Instalar Node.js y elegir una versión que cumpla lo que exigen Vite y React Router
  • Manejar npm con criterio: dependencias, rangos de versiones, package-lock.json, npm ci y npx
  • Crear un proyecto con Vite y la plantilla react-ts, evitando las trampas de PowerShell y de la opción ESLint
  • Recorrer el proyecto generado y saber para qué sirve cada fichero
  • Seguir el arranque de la aplicación desde index.html hasta el primer componente pintado
  • Configurar Vite: puerto, proxy hacia la API EMPRESA, alias de rutas y variables de entorno
  • Comprender cómo funcionan HMR y Fast Refresh, y por qué a veces se pierde el estado
  • Conocer TypeScript 7 y leer los tres tsconfig de la plantilla opción por opción
  • Revisar y dar formato al código con oxlint y Prettier
  • Preparar VS Code y usar React DevTools para inspeccionar componentes
  • Generar la versión de producción, probarla con preview y publicarla en una subcarpeta
  • Instalar una versión concreta de React sin caer en la trampa del ^

1. Por qué hacen falta tantas piezas

Un navegador sabe ejecutar HTML, CSS y JavaScript. No sabe qué es JSX ni qué es TypeScript. Si escribes esto en un fichero y lo cargas con una etiqueta <script>, obtienes un error de sintaxis:

function Saludo({ nombre }: { nombre: string }) {
  return <h1>Hola, {nombre}</h1>
}

La línea tiene dos cosas que no son JavaScript: la anotación de tipos (: { nombre: string }) y la etiqueta <h1> en mitad del código. Por eso necesitas un paso previo que convierta tu código en JavaScript normal. Además, una aplicación real se reparte en cientos de ficheros y usa bibliotecas instaladas con npm, y eso plantea más trabajo: resolver los import, optimizar para producción, ver los cambios al guardar sin perder lo que tenías en pantalla, y detectar errores antes de que los vea un usuario.

Antes de Vite, ese trabajo lo hacía sobre todo Create React App, que ya está retirado (el equipo de React lo anunció en febrero de 2025). Hoy hay dos caminos: una herramienta de construcción como Vite, que es lo que usarás durante casi todo el curso, o un framework como Next.js, que verás en el módulo 23. Vite te deja ver cómo funciona React sin capas adicionales, y por eso empezamos con él.

Esquema con el código fuente, las herramientas que lo revisan, el servidor de desarrollo, la construcción para producción y Node.js como base

Figura 1. Cada herramienta hace una cosa. Ninguna de ellas forma parte de tu aplicación final, salvo React.

Pieza Versión del curso Qué hace Cuándo actúa
Node.js 24.19.0 (o cualquier LTS actual) Ejecuta las herramientas en tu ordenador Siempre, por debajo
npm 11.6.2 Instala paquetes y lanza scripts Al instalar y al ejecutar órdenes
React y React DOM 19.3.1 La biblioteca con la que construyes la interfaz En el navegador del usuario
Vite 8.3.5 Servidor de desarrollo y construcción para producción Mientras programas y al publicar
@vitejs/plugin-react 6.1.2 Enseña a Vite a tratar JSX y activa Fast Refresh Dentro de Vite
TypeScript 7.0.2 Comprueba que los tipos son correctos En el editor y en npm run build
oxlint 1.90 Detecta errores y malas prácticas En npm run lint y en el editor
Prettier 3.9 Da un formato uniforme al código Al guardar o en npm run format
React DevTools 8.0 Inspecciona componentes en el navegador Mientras depuras

📘 Concepto: Separa mentalmente tres momentos. Desarrollo: tú en tu ordenador, con Vite sirviendo el código. Construcción (build): se genera la carpeta dist/ con ficheros optimizados. Ejecución: el navegador del usuario carga esos ficheros. En el tercer momento ya no existen Node, Vite ni TypeScript: solo HTML, CSS y JavaScript.

Si vienes de otro curso, esto te resultará familiar por otro lado: es parecido a compilar Java con Maven y ejecutar el .jar. La diferencia es que aquí el resultado no se ejecuta en un servidor tuyo, sino en el navegador de cada visitante.


2. Node.js: instalación y versiones

Node.js es un entorno que ejecuta JavaScript fuera del navegador. En este curso no lo usas para programar un servidor: lo usas porque Vite, TypeScript, oxlint y npm son programas que necesitan Node para arrancar. La aplicación que escribas se ejecutará en el navegador, sin Node.

2.1. Qué versión necesitas

Node publica una versión mayor cada seis meses. Las pares pasan a ser LTS (Long Term Support, soporte de larga duración) y son las que conviene usar. Estas son las líneas LTS activas a fecha de este curso:

Línea Nombre Última publicada Situación
Node 26 Lithium 26.13.0 LTS más reciente
Node 24 Krypton 24.22.2 LTS. Los ejemplos del curso se han ejecutado con 24.19.0
Node 22 Jod 22.23.4 LTS más antigua

Algunas herramientas del curso ponen un mínimo, y conviene conocerlo porque un Node antiguo es la primera causa de errores raros al arrancar:

Herramienta Exige Node
Vite 8 y create-vite ^20.19.0 \|\| >=22.12.0
API de ejemplo EMPRESA 22 o superior
React Router 8 (módulo 18) 22.22.0 o superior

💡 Consejo: Instala Node 24 o Node 26. Cumplen todos los requisitos del curso. La versión 20 te serviría para Vite, pero se quedaría corta para la API y para React Router.

2.2. Instalar Node

La forma más directa es el instalador oficial de https://nodejs.org: elige la versión LTS y acepta las opciones por defecto. En Windows también puedes usar winget:

winget install OpenJS.NodeJS.LTS

Si vas a trabajar en varios proyectos, un gestor de versiones es mejor opción, porque te permite tener varias versiones de Node instaladas y cambiar de una a otra. fnm funciona en Windows, macOS y Linux:

winget install Schniz.fnm      # en Windows (en macOS: brew install fnm)
fnm install 24
fnm use 24
fnm default 24

Cierra la terminal y abre otra después de instalar cualquier cosa: las variables de entorno solo se leen al abrirla. Después comprueba que todo responde:

node -v
npm -v
v24.19.0
11.6.2

2.3. El error de PowerShell con npm.ps1

En algunos Windows, la primera vez que escribes npm en PowerShell aparece un mensaje que dice que no se puede cargar npm.ps1 porque la ejecución de scripts está deshabilitada. No es un problema de Node, sino de la política de ejecución de PowerShell. Se arregla para tu usuario con:

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

Otra salida es usar Git Bash, que además te ahorra la trampa de la sección 4.2.


3. npm en lo que necesitas saber

npm hace dos trabajos: descarga paquetes del registro público y ejecuta los scripts de tu proyecto. Casi todo gira alrededor de un fichero, package.json.

3.1. package.json

Este es el que genera la plantilla, sin cambiar nada:

{
  "name": "mi-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "oxlint",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.3.1",
    "react-dom": "^19.3.1"
  },
  "devDependencies": {
    "@types/node": "^24.19.2",
    "@types/react": "^19.3.0",
    "@types/react-dom": "^19.3.0",
    "@vitejs/plugin-react": "^6.1.2",
    "oxlint": "^1.89.0",
    "typescript": "~7.0.2",
    "vite": "^8.3.5"
  }
}
Campo Significado
name, version Identifican el paquete. En una aplicación apenas importan
private: true Impide publicarlo por accidente en el registro de npm
type: "module" Los .js de este proyecto usan import/export, no require
scripts Órdenes con nombre que lanzas con npm run <nombre>
dependencies Lo que forma parte de la aplicación que recibe el usuario
devDependencies Lo que solo se usa para desarrollar y construir

Fíjate en "build": "tsc -b && vite build". El && significa “ejecuta lo segundo solo si lo primero ha ido bien”. Si tsc encuentra un error de tipos, no llega a construirse nada. Volveremos a esto en la sección 9.

¿Por qué react va en dependencies y vite en devDependencies? Porque el código de React acaba dentro del JavaScript que descarga el usuario, y Vite no. En una aplicación que se empaqueta, esta separación no cambia el resultado, porque el empaquetador solo incluye lo que importas, pero documenta qué es cada cosa.

Los scripts encuentran vite y tsc porque npm añade node_modules/.bin a la ruta mientras los ejecuta. Por eso funcionan sin instalar nada de forma global, y cada proyecto usa su propia versión.

3.2. Los números de versión

Los paquetes siguen el versionado semántico (semver): MAYOR.MENOR.PARCHE. Una versión mayor puede romper código existente, una menor añade funciones sin romper nada, y un parche corrige errores. Los símbolos delante del número definen qué versiones aceptas:

Escrito Acepta Ejemplo con 19.3.1
^19.3.1 Cambios menores y parches 19.3.2, 19.4.0, pero no 20.0.0
~7.0.2 Solo parches 7.0.3, pero no 7.1.0
19.3.1 Esa versión exacta Solo 19.3.1
latest Lo que marque la etiqueta en ese momento Hoy, 19.3.1

La plantilla usa ~ con TypeScript y no ^, y tiene una buena razón: TypeScript no sigue semver al pie de la letra. Una versión menor puede detectar errores nuevos y hacer que deje de compilar código que antes pasaba.

3.3. package-lock.json y node_modules

package.json dice lo que pides, pero con rangos. Dos personas que instalen en días distintos podrían obtener versiones diferentes. Para evitarlo, npm anota en package-lock.json la versión exacta de cada paquete del árbol completo, con una suma de comprobación.

Tres cajas: package.json con los rangos, package-lock.json con las versiones exactas y node_modules con los ficheros en disco

Figura 2. El lock es parte de tu código: súbelo a Git. node_modules es un resultado: nunca lo subas.

Orden Qué hace
npm install Instala todo lo de package.json, respetando el lock si encaja
npm install zod Añade un paquete a dependencies
npm install -D prettier Añade un paquete a devDependencies
npm install -E react@19.1.10 Añade la versión exacta, sin ^
npm uninstall zod Quita un paquete
npm ci Borra node_modules e instala exactamente lo que dice el lock
npm outdated Muestra qué paquetes tienen versiones más nuevas
npm update Actualiza dentro de los rangos permitidos
npm ls react Muestra qué versión hay instalada y quién la pide
npm audit Busca vulnerabilidades conocidas en tus dependencias
npx <paquete> Ejecuta un paquete: el local si existe, y si no lo descarga para esa ocasión

⚠️ Importante: Si algo se estropea en node_modules, no intentes arreglarlo a mano. Bórralo y ejecuta npm install. Si el problema persiste, borra también package-lock.json y vuelve a instalar, sabiendo que entonces las versiones pueden cambiar dentro de los rangos.

npm install en una plantilla recién creada añade unos 28 paquetes y tarda alrededor de un minuto. Puedes usar otros gestores (pnpm, Yarn, Bun), que son más rápidos y ahorran disco, pero npm viene con Node y es el que usará el curso.


4. Crear el proyecto

4.1. La orden

Colócate en la carpeta donde guardes tus proyectos y ejecuta, en Git Bash, macOS o Linux:

npm create vite@latest mi-app -- --template react-ts

Vamos por partes:

  • npm create vite@latest descarga y ejecuta el paquete create-vite en su última versión. En este curso, la 9.3.3.
  • mi-app es la carpeta que se creará.
  • -- separa las opciones de npm de las opciones que se pasan al programa. Todo lo que va detrás no es para npm, sino para create-vite.
  • --template react-ts elige la plantilla de React con TypeScript.
create-vite v9.3.3

|
o  Scaffolding project in .../mi-app...
|
—  Done. Now run:

  cd mi-app
  npm install
  npm run dev

4.2. La trampa de PowerShell

En Windows PowerShell 5.1, ese -- no llega a npm: PowerShell se lo queda. El resultado es desconcertante, porque la orden parece funcionar, pero npm interpreta --template como una opción suya. Esto es lo que se obtiene de verdad:

npm warn "react-ts" is being parsed as a normal command line argument.
npm warn Unknown cli config "--template". This will stop working in the next major version of npm.

Si no te das cuenta de los avisos, create-vite no recibe la plantilla. En nuestra prueba, sin modo interactivo, generó la plantilla vanilla con TypeScript: tienes src/main.ts, src/counter.ts y src/style.css, y no hay rastro de React. Con el modo interactivo, simplemente te preguntará lo que le falta.

La solución es entrecomillar el --:

npm create vite@latest mi-app "--" --template react-ts

Con esa orden sí aparecen src/App.tsx y src/main.tsx. La prueba se hizo en Windows PowerShell 5.1. Si usas otra versión, entrecomillar el -- no tiene efectos secundarios, así que puedes hacerlo siempre que estés en PowerShell. Pasa lo mismo cada vez que uses -- para reenviar opciones, como en npm run dev "--" --port 3001.

💡 Consejo: Para comprobar que tienes la plantilla correcta, mira si existe src/main.tsx. Si ves main.ts, sin la x, no es la plantilla de React. Borra la carpeta y repite la orden.

4.3. El modo interactivo

Si escribes solo npm create vite@latest, el programa te hace preguntas. Son estas, en este orden:

Pregunta Qué elegir en este curso
Project name: El nombre de la carpeta
Select a framework: React
Select a variant: TypeScript
Which linter to use? Oxlint
Install with npm and start now? Lo que prefieras

En la variante aparecen más opciones de las que usaremos: TypeScript + React Compiler (el compilador se explica en el módulo 14), las versiones JavaScript, RSC (Server Components, en el módulo 23) y React Router v7. Esta última lanza el asistente de ese proyecto, que no es el que seguimos en el curso, porque el módulo 18 usa React Router 8.

⚠️ Importante: Elige Oxlint, no ESLint. La opción ESLint de create-vite 9.3.3 genera un package.json que no se puede instalar. Pide typescript ~7.0.2 y a la vez typescript-eslint ^8.72.0, que solo admite TypeScript por debajo de 6.1. npm install termina con este error:

npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error peer typescript@">=4.8.4 <6.1.0" from typescript-eslint@8.72.1

La causa se explica en la sección 10.4. Es posible que una versión posterior lo arregle, pero conviene saberlo si ves ese mensaje.

4.4. Instalar y arrancar

cd mi-app
npm install
npm run dev

La salida de npm run dev tiene este aspecto (el tiempo cambia mucho de un ordenador a otro, y la primera vez siempre es la más lenta):

Port 5173 is in use, trying another one...

  VITE v8.3.5  ready in 9172 ms

  ➜  Local:   http://localhost:5174/
  ➜  Network: use --host to expose

En esta ejecución, el puerto por defecto, 5173, estaba ocupado por otro programa, y por eso aparece la primera línea y sale 5174. Lo normal es que no veas ese aviso y que la dirección sea http://localhost:5173/, que es la que usaremos en el curso. La sección 8.4 lo explica.

Abre esa dirección. Para parar el servidor, pulsa Ctrl + C en la terminal. Déjalo arrancado mientras programas: no hay que relanzarlo cada vez que guardas.

Página de bienvenida de la plantilla de Vite con los logotipos de React y Vite, el texto Get started y un botón contador

Figura 3. Página inicial de la plantilla react-ts. El botón es un contador real: compruébalo.


5. Recorrido por el proyecto

mi-app/
├── node_modules/          Paquetes instalados (no se sube a Git)
├── public/                Ficheros que se copian tal cual
│   ├── favicon.svg
│   └── icons.svg
├── src/                   Tu código
│   ├── assets/            Imágenes que importa el código
│   │   ├── hero.png
│   │   ├── react.svg
│   │   └── vite.svg
│   ├── App.css            Estilos del componente App
│   ├── App.tsx            Componente raíz
│   ├── index.css          Estilos globales
│   └── main.tsx           Punto de entrada de JavaScript
├── .gitignore             Lo que Git debe ignorar
├── .oxlintrc.json         Configuración del linter
├── index.html             Punto de entrada de la aplicación
├── package.json           Dependencias y scripts
├── package-lock.json      Versiones exactas (aparece tras npm install)
├── README.md              Notas de la plantilla
├── tsconfig.json          Agrupa las dos configuraciones siguientes
├── tsconfig.app.json      TypeScript para el código de src/
├── tsconfig.node.json     TypeScript para vite.config.ts
└── vite.config.ts         Configuración de Vite

Dos detalles sorprenden a quien viene de otras herramientas. El primero es que index.html está en la raíz, no en public/: para Vite, ese fichero es la puerta de entrada al proyecto, y lo procesa. El segundo es que no hay una carpeta dist/: no aparece hasta que ejecutas npm run build.

public/ frente a src/assets/

Hay dos sitios para guardar imágenes y otros recursos, y no son intercambiables.

  src/assets/ (u otra carpeta de src/) public/
Cómo se usa import logo from './assets/logo.svg' Por ruta absoluta: /favicon.svg
Pasa por Vite Sí No, se copia sin tocar
Nombre final Con huella: react-CHdo91hT.svg El original: favicon.svg
Si el fichero no existe Falla al compilar Falla en el navegador, en silencio
Ficheros pequeños Se incrustan en el JavaScript Se copian igual
Ideal para Imágenes de la interfaz favicon, robots.txt, ficheros con nombre fijo

La huella (hash) en el nombre depende del contenido. Si la imagen cambia, cambia el nombre. Así el servidor puede decir a los navegadores que guarden el fichero para siempre, porque una versión nueva tendrá otra URL.

Lo de incrustar lo hemos comprobado con dos SVG: uno de 110 bytes y otro de 4,12 kB. El límite por defecto es de 4 KiB. El pequeño no generó ningún fichero y quedó dentro del JavaScript como data:image/svg+xml,..., mientras que el grande acabó en dist/assets/grande-CHdo91hT.svg. Te ahorra una petición HTTP sin que tengas que hacer nada.

import logo from './assets/logo.svg'   // logo es un string con la URL final

function Marca() {
  return <img src={logo} alt="Logotipo de EMPRESA" />
}

⚠️ Importante: Importa siempre lo que esté en src/. Una ruta como <img src="/src/assets/logo.svg"> parece funcionar en desarrollo, porque el servidor de Vite la encuentra, y falla en producción, porque esa ruta no existe en dist/.


6. Cómo arranca la aplicación

Antes de cambiar nada, sigue el camino que recorre el navegador. Entender esto evita muchas dudas más adelante.

Tres pasos: index.html con el div root y el script, main.tsx con createRoot y render, y App.tsx con sus componentes, que acaban dentro de root

Figura 4. El HTML solo aporta un contenedor vacío. El resto lo crea JavaScript.

6.1. index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>mi-app</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Hay dos líneas importantes. <div id="root"> es el contenedor donde React pintará la aplicación, y el <script type="module"> carga un fichero .tsx, cosa que un navegador no entendería por sí solo. Funciona porque quien responde a esa petición no es un servidor de ficheros cualquiera, sino Vite, que transforma el fichero antes de entregarlo.

Si miras “Ver código fuente” en el navegador, verás este HTML casi vacío. Es lo que se llama una SPA (Single Page Application): hay una sola página HTML, y JavaScript construye toda la interfaz en el cliente. Esto tiene consecuencias en carga inicial y en SEO que se tratan en el módulo 23.

6.2. src/main.tsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Línea a línea:

  • import { StrictMode } from 'react': react contiene el núcleo, común a todos los entornos.
  • import { createRoot } from 'react-dom/client': react-dom es la parte que sabe trabajar con el DOM del navegador. Son paquetes separados porque React también puede pintar en otros destinos, como aplicaciones móviles con React Native.
  • import './index.css': importar un CSS desde JavaScript no es estándar. Lo resuelve Vite, que lo inyecta en la página y lo extrae a un .css al construir.
  • import App from './App.tsx': importa el componente raíz. La extensión .tsx en el import es válida porque tsconfig.app.json activa allowImportingTsExtensions.
  • document.getElementById('root')!: getElementById puede devolver null. La ! es un operador de TypeScript que significa “te garantizo que aquí hay un valor”. Es seguro porque el <div> está en index.html. Si renombras ese id, TypeScript no te avisará y fallará al ejecutarse.
  • createRoot(...): crea una raíz de React sobre ese nodo, y .render(...) pinta dentro de ella.
  • <StrictMode>: activa comprobaciones adicionales solo en desarrollo.

📘 Concepto: <StrictMode> ejecuta dos veces, a propósito, el cuerpo de los componentes para sacar a la luz código que no es puro. Lo hemos medido poniendo un console.log dentro de un componente: en desarrollo aparece dos veces en cada carga, y en la versión construida, una sola. Si ves logs duplicados, no es un error. Se explica a fondo en el módulo 05.

6.3. src/App.tsx

Un componente es una función que devuelve lo que hay que pintar, con el nombre en mayúscula. En el módulo 02 verás JSX con detalle, y en el módulo 03, los componentes. De momento basta esta idea: <App /> dentro de render hace que React llame a la función App y convierta lo que devuelve en nodos del DOM dentro de #root.


7. Primer cambio: de la plantilla a EMPRESA

En todo el curso trabajarás con la aplicación de gestión EMPRESA, así que vamos a quitar lo que trae la plantilla y a dejar una base propia. Es la operación que harás al empezar cualquier proyecto.

Borra src/App.css, src/assets/hero.png, y quita de index.html el título por defecto:

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>EMPRESA</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Cambiar lang a es no es un detalle menor: es lo que usan los lectores de pantalla para elegir la voz y el traductor del navegador para saber el idioma de partida.

Crea una carpeta src/componentes/ con dos componentes. No te preocupes por entender todos los detalles, porque useState se explica en el módulo 04. Lo necesitamos aquí para tener algo con estado con lo que probar Fast Refresh.

// src/componentes/Cabecera.tsx
function Cabecera({ titulo }: { titulo: string }) {
  return (
    <header>
      <h1>{titulo}</h1>
      <p>Gestión de empleados, productos y pedidos</p>
    </header>
  )
}

export default Cabecera
// src/componentes/Contador.tsx
import { useState } from 'react'

function Contador() {
  const [pedidos, setPedidos] = useState(0)

  return (
    <button type="button" onClick={() => setPedidos((n) => n + 1)}>
      Pedidos registrados: {pedidos}
    </button>
  )
}

export default Contador

Sustituye src/App.tsx:

// src/App.tsx
import { version } from 'react'
import Cabecera from './componentes/Cabecera.tsx'
import Contador from './componentes/Contador.tsx'

function App() {
  return (
    <>
      <Cabecera titulo="EMPRESA" />
      <main>
        <p>
          Proyecto creado con Vite y React {version}. Modo:{' '}
          <strong>{import.meta.env.MODE}</strong>
        </p>
        <Contador />
      </main>
    </>
  )
}

export default App

version es una exportación real de React con la versión instalada. Es una forma cómoda de comprobar qué estás ejecutando de verdad. Por último, sustituye src/index.css. Usamos light-dark(), una función de CSS que elige el color según el tema del sistema, así el tema oscuro sale gratis. Puedes repasarla en el Curso de HTML y CSS.

/* src/index.css */
:root {
  color-scheme: light dark;
  font-family: system-ui, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.5;
  background: light-dark(#f5f6fa, #15171f);
  color: light-dark(#1f2333, #e6e8f0);
}

body {
  margin: 0;
}

header {
  padding: 1.5rem 2rem;
  background: light-dark(#283593, #1a237e);
  color: #fff;
}

header h1 {
  margin: 0;
  font-size: 1.75rem;
}

header p {
  margin: 0;
  opacity: 0.85;
}

main {
  padding: 1.5rem 2rem;
}

button {
  padding: 0.6rem 1.1rem;
  border: 0;
  border-radius: 0.5rem;
  font: inherit;
  color: #fff;
  background: #3f51b5;
  cursor: pointer;
}

button:hover {
  background: #283593;
}

Aplicación EMPRESA con cabecera azul, el texto que indica React 19.3.1 en modo development y un botón Pedidos registrados

Figura 5. La base del proyecto, con el tema claro del sistema.

La misma aplicación EMPRESA con fondo oscuro y texto claro

Figura 6. La misma página cuando el sistema usa el tema oscuro. El CSS no ha cambiado.


8. Vite a fondo

Vite significa “rápido” en francés, y se pronuncia “vit”. Su idea principal es no hacer el mismo trabajo en desarrollo que en producción.

8.1. Desarrollo: módulos sueltos

Los navegadores modernos entienden import de forma nativa (<script type="module">), y Vite se aprovecha de ello. En desarrollo no empaqueta tu código. Cuando el navegador pide un fichero, Vite lo transforma en ese momento y se lo entrega, y el navegador sigue tirando del hilo de los import.

Hemos registrado las peticiones reales que hace la página EMPRESA al cargar. Son 14 (rutas de dependencias acortadas):

/
/@vite/client
/src/main.tsx
/@react-refresh
.../.vite/deps/react.js
.../.vite/deps/react-dom_client.js
/src/index.css
/src/App.tsx
.../.vite/deps/react_jsx-dev-runtime.js
.../vite/dist/client/env.mjs
.../.vite/deps/react-CtSWVglQ.js
/src/componentes/Cabecera.tsx
/src/componentes/Contador.tsx
.../.vite/deps/react-dom-Cr7J_nE3.js

Puedes verlo tú: pulsa F12, abre la pestaña Red (Network) y recarga. Hay tres cosas que merece la pena observar:

  • /@vite/client: un pequeño programa que Vite añade a la página. Abre un WebSocket con el servidor para recibir avisos de cambios.
  • /@react-refresh: el motor de Fast Refresh.
  • .vite/deps/: tus dependencias no se sirven tal cual. React son decenas de ficheros en formato CommonJS, así que la primera vez Vite los preempaqueta en módulos ESM y los guarda en node_modules/.vite/deps. Esto se ve en la terminal como [optimizer] bundling dependencies..., y es la razón de que el primer arranque sea más lento.

¿Qué recibe el navegador cuando pide /src/componentes/Contador.tsx? Esto es lo que entrega Vite, sacado directamente de la respuesta y con las rutas largas acortadas:

var _s = $RefreshSig$();
function Contador() {
	_s();
	const [pedidos, setPedidos] = useState(0);
	return /* @__PURE__ */ _jsxDEV("button", {
		type: "button",
		onClick: () => setPedidos((n) => n + 1),
		children: ["Pedidos registrados: ", pedidos]
	}, void 0, true, {
		fileName: _jsxFileName,
		lineNumber: 7,
		columnNumber: 5
	}, this);
}
_s(Contador, "PatmNOetW6XadE3Rt5zNzx5uITk=");
_c = Contador;
export default Contador;
var _c;
$RefreshReg$(_c, "Contador");

Aquí se ven varias ideas importantes. La etiqueta <button> se ha convertido en una llamada a una función (_jsxDEV), que es lo que realmente es JSX (módulo 02). Las anotaciones de tipos han desaparecido. Y han aparecido llamadas a $RefreshSig$ y $RefreshReg$, con las que Fast Refresh registra cada componente. La cadena "PatmNOetW6Xa..." es una huella de los hooks del componente, y la sección 9 explica para qué sirve.

Quien hace la transformación es Oxc, una cadena de herramientas escrita en Rust. Vite 8 lo usa en lugar de Babel y esbuild.

8.2. Producción: un paquete optimizado

En producción, la estrategia anterior sería mala. Decenas de peticiones por cada carga penalizan a los usuarios, sobre todo con conexiones lentas. Por eso vite build hace lo contrario y empaqueta con Rolldown, un empaquetador en Rust que en Vite 8 sustituye a la pareja esbuild y Rollup: junta los módulos, elimina el código que no se usa (tree shaking), minifica y pone huellas en los nombres.

Comparación entre desarrollo, con el servidor Vite sirviendo muchos módulos sueltos, y producción, con los módulos empaquetados en la carpeta dist

Figura 7. Dos estrategias para dos necesidades: rapidez de arranque al programar y descarga mínima para el usuario.

Hay una diferencia importante entre ambos modos además de la velocidad: en desarrollo usas la versión de desarrollo de React, con avisos detallados, y en producción, una versión reducida sin ellos. Por eso una aplicación debe medirse siempre con la versión construida.

8.3. vite.config.ts

La plantilla trae lo mínimo:

import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
})

defineConfig no hace nada en ejecución: solo aporta los tipos, para que el editor te complete las opciones. Estas son las más útiles al empezar:

Opción Para qué sirve Ejemplo
server.port Puerto del servidor de desarrollo 5173 por defecto
server.strictPort Falla si el puerto está ocupado, en vez de buscar otro true
server.host Escuchar en la red local true
server.open Abrir el navegador al arrancar true
server.proxy Reenviar rutas a otro servidor Sección 8.5
resolve.tsconfigPaths Respetar los paths de tsconfig Sección 8.6
base Ruta pública si no publicas en la raíz '/empresa/'
build.outDir Carpeta de salida dist por defecto
build.sourcemap Generar mapas de código en producción true

Cada cambio en vite.config.ts reinicia el servidor automáticamente. Muchas opciones existen también en la línea de órdenes. Para pasarlas a través de npm run hace falta otra vez el --:

npm run dev -- --port 3001 --open

8.4. Puertos y acceso desde otros dispositivos

Si el puerto 5173 está ocupado, por ejemplo porque tienes otro proyecto en marcha, Vite elige el siguiente libre y lo avisa, como viste en la sección 4.4. Fíjate bien en la URL. Es fácil seguir mirando la pestaña antigua sin darse cuenta de que estás viendo otro proyecto. Si no quieres este comportamiento, --strictPort hace que falle:

error when starting dev server:
Error: Port 5173 is already in use

Por defecto, el servidor solo responde en tu propio ordenador. Para probar la aplicación en el móvil, conectado a la misma red, arranca con npm run dev -- --host. La terminal mostrará una línea Network: por cada interfaz de red. Si tu cortafuegos pregunta, permite el acceso solo en redes privadas.

8.5. El proxy hacia la API EMPRESA

El curso usa una API de ejemplo que se descarga como recursos/empresa-api.zip. Solo necesita Node, sin dependencias:

node server.mjs

Queda escuchando en http://localhost:3000/api. Tu aplicación, en cambio, está en http://localhost:5173. Son orígenes distintos, porque cambia el puerto, y los navegadores bloquean por defecto las peticiones entre orígenes salvo que el servidor las autorice con cabeceras CORS. Además, escribir la URL completa en el código te obligaría a cambiarla al publicar.

La solución habitual en desarrollo es un proxy: tu código pide /api/empleados a su propio origen, y el servidor de Vite reenvía la petición a la API.

// vite.config.ts
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://localhost:3000',
    },
  },
})

El navegador pide /api al servidor de Vite en el puerto 5173, que reenvía la petición a la API en el puerto 3000

Figura 8. Para el navegador, todo viene del mismo origen. El reenvío ocurre entre los dos servidores.

Con la API y Vite en marcha, abre la consola del navegador en tu aplicación y prueba:

await fetch('/api/departamentos?tamano=2').then((r) => r.json())
{"contenido":[{"deptId":10,"nombre":"DIRECCIÓN","localidad":"Madrid","presupuesto":500000},{"deptId":20,"nombre":"VENTAS","localidad":"Madrid","presupuesto":350000}],"pagina":1,"tamano":2,"total":7,"paginas":4}

⚠️ Importante: El proxy es una ayuda del servidor de desarrollo. npm run build no lo incluye, y en producción esa ruta /api tiene que resolverla el servidor real. Se trata en el módulo 25.

8.6. Alias de rutas

Cuando las carpetas crecen, aparecen importaciones como ../../../componentes/Boton.tsx, que se rompen al mover un fichero. Un alias permite escribir @/componentes/Boton.tsx, con @ como sinónimo de src.

Hay que decírselo a dos programas distintos, porque son independientes: TypeScript comprueba los tipos, y Vite resuelve los ficheros. En Vite 8 la segunda parte es de una sola línea, porque puede leer la configuración de TypeScript:

// tsconfig.app.json, dentro de compilerOptions
"paths": { "@/*": ["./src/*"] },
// vite.config.ts
export default defineConfig({
  plugins: [react()],
  resolve: {
    tsconfigPaths: true,
  },
})
import Cabecera from '@/componentes/Cabecera.tsx'

⚠️ Importante: Muchos tutoriales añaden "baseUrl": "." junto a paths. En TypeScript 7 esa opción ya no existe, y falla: error TS5102: Option 'baseUrl' has been removed. Con paths y rutas que empiezan por ./ es suficiente.

8.7. Variables de entorno

Algunos valores cambian según dónde se ejecute la aplicación: la URL de la API, el nombre que se muestra, etc. Se guardan en ficheros .env en la raíz del proyecto:

# .env
VITE_NOMBRE_APP=EMPRESA
VITE_API_URL=/api
CLAVE_SECRETA=no-debe-llegar-al-navegador

Se leen con import.meta.env:

<Cabecera titulo={import.meta.env.VITE_NOMBRE_APP} />

Aquí está la regla clave: solo las variables que empiezan por VITE_ llegan al código del navegador. Después de construir este ejemplo, hemos buscado los dos valores en dist/: EMPRESA aparece en el JavaScript, y no-debe-llegar-al-navegador no aparece en ningún fichero.

⚠️ Importante: El prefijo VITE_ significa “publica este valor”. Todo lo que llega al navegador lo puede leer cualquier visitante con F12. Nunca pongas contraseñas ni claves de API privadas en una variable VITE_. En una aplicación de navegador no existen los secretos.

Vite también define algunas variables por su cuenta:

Variable Tipo Valor
import.meta.env.MODE string development en dev, production en build
import.meta.env.DEV boolean true en desarrollo
import.meta.env.PROD boolean true en la versión construida
import.meta.env.BASE_URL string El valor de base

Los valores se sustituyen al construir. No son variables que se lean en el servidor al ejecutarse: una vez generado dist/, cambiar .env no tiene efecto hasta que vuelvas a construir.

Puedes tener un fichero por modo, y el más específico gana:

Fichero Cuándo se carga ¿A Git?
.env Siempre Sí, sin secretos
.env.local Siempre No (lo ignora *.local)
.env.production En vite build Sí
.env.staging Con vite build --mode staging Sí

Lo hemos comprobado: con .env.production definiendo VITE_NOMBRE_APP=EMPRESA PROD, vite build incluyó ese valor, y vite build --mode staging tomó el de .env.staging.

Al guardar un .env con el servidor arrancado, Vite lo detecta, se reinicia solo y la página se recarga, así que se pierde el estado:

1:58:45 [vite] .env changed, restarting server...
1:58:48 [vite] server restarted.

Por defecto, TypeScript no conoce tus variables: las trata como any. Para tener autocompletado y el tipo correcto, decláralas en un fichero src/env.d.ts. La primera interfaz es opcional, y es la que hace que una errata se convierta en error:

// src/env.d.ts
interface ViteTypeOptions {
  strictImportMetaEnv: unknown
}

interface ImportMetaEnv {
  readonly VITE_NOMBRE_APP: string
  readonly VITE_API_URL: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

Sin strictImportMetaEnv, import.meta.env.VITE_NOMBRE compila y vale undefined, porque los tipos de Vite admiten cualquier clave. Con ella, tsc responde:

error TS2339: Property 'VITE_NOMBRE' does not exist on type 'ImportMetaEnv'.

9. HMR y Fast Refresh

Hace años, cada cambio obligaba a recargar la página. Si estabas probando el tercer paso de un formulario, tenías que rellenar los dos anteriores otra vez. Dos mecanismos lo evitan:

  • HMR (Hot Module Replacement, sustitución de módulos en caliente): es el mecanismo general de Vite. Al guardar, el servidor avisa por WebSocket, el navegador pide solo el módulo cambiado y lo sustituye sin recargar.
  • Fast Refresh: es la parte específica de React, integrada por @vitejs/plugin-react. Sabe volver a pintar un componente con el código nuevo conservando su estado.

9.1. Qué ocurre en cada caso

Para no repetir lo que suele decirse, lo hemos medido. Se abre la página EMPRESA, se pulsa el contador tres veces, se deja una marca en window que desaparecería con una recarga, se edita un fichero y se mira qué ha pasado:

Qué editas Contador ¿Recarga? Mensaje en la terminal
El texto del botón Conserva 3 No hmr update
index.css Conserva 3 No hmr update /src/index.css
El valor inicial: useState(10) Pasa a 10 No hmr update
Añades otro useState Pasa a 0 No hmr update
Arreglas un error de sintaxis Conserva 3 No hmr update
Contador.tsx exporta además una constante o función Conserva 3 No hmr invalidate
App.tsx exporta además una constante Pasa a 0 Sí hmr invalidate y page reload
main.tsx Pasa a 0 Sí page reload src/main.tsx
.env o vite.config.ts Pasa a 0 Sí changed, restarting server

Fíjate en dos filas. Cambiar el valor inicial de useState reinicia el estado, aunque parezca que debería conservarse: esa es la función de la huella que vimos en la sección 8.1, "PatmNOetW6Xa...". Fast Refresh calcula una firma con los hooks de cada componente y sus argumentos. Si la firma cambia, no se fía del estado viejo y monta el componente de nuevo, porque intentar mezclar el estado antiguo con hooks distintos podría romperlo.

La segunda es el caso de las exportaciones mezcladas, que no siempre acaba como dice mucha documentación.

Diagrama de decisión: según lo que exporte el fichero y qué hooks use, el resultado es Fast Refresh, remontar el componente, actualizar el padre o una recarga completa

Figura 9. Las cuatro salidas posibles al guardar. Solo la de la derecha pierde todo.

9.2. Por qué el linter avisa de las exportaciones mezcladas

Para que un módulo sea una frontera de actualización, Fast Refresh exige que todo lo que exporta sean componentes. Si exportas algo más, no puede saber si quien importa esa función o constante debe actualizarse también. Vite entonces escribe:

hmr invalidate /src/componentes/Contador.tsx Could not Fast Refresh ("MAXIMO" export is incompatible).

invalidate significa “yo no puedo con esto, que se ocupe quien me importa”. La actualización sube un nivel. En nuestra prueba, Contador.tsx lo importa App.tsx, que sí acepta actualizaciones, y todo acabó bien, con el estado intacto. Pero App.tsx lo importa main.tsx, que no acepta ninguna, así que al subir no encontró quién la recogiera y Vite tuvo que recargar la página.

La solución es sencilla: mueve las constantes y funciones de ayuda a otro fichero. Es lo que comprueba la regla react/only-export-components de oxlint, activada en la plantilla.

// src/limites.ts
export const MAXIMO_PEDIDOS = 5
// src/componentes/Contador.tsx: solo exporta el componente
import { MAXIMO_PEDIDOS } from '../limites.ts'

💡 Consejo: Si buscas información verás que existe un comentario, // @refresh reset, para forzar que un componente se reinicie siempre. En esta pila (Vite 8 con @vitejs/plugin-react 6, que usa Oxc) no ha tenido efecto en nuestra prueba: el contador conservó su valor al editar el fichero. No cuentes con él. Si un cambio te deja la pantalla en un estado raro, recarga con F5.

9.3. Tres clases de error en desarrollo

Error de sintaxis. El fichero no se puede transformar. Vite muestra una capa sobre la página con el fichero y la línea. Al corregirlo, la capa desaparece sola, y el estado se conserva: en la prueba, el contador seguía en 3.

Capa de error de Vite sobre la aplicación con el texto Transform failed y PARSE_ERROR, que señala Contador.tsx línea 12

Figura 10. La capa de error de Vite ante un error de sintaxis (se ha abierto un <div> sin cerrar).

El mensaje Unexpected token en la línea 12, que es la última, no es donde está el fallo: el analizador solo se da cuenta del <div> sin cerrar cuando llega al final. Mira hacia arriba. Es un comportamiento habitual en cualquier compilador.

Error de tipos. Si escribes const malo: number = 'texto', Vite no dice nada. La página sigue funcionando, sin capa ni aviso. Es lo que veremos en la sección 10.1: en desarrollo, nadie comprueba los tipos salvo tu editor.

Error en ejecución. Si un componente lanza una excepción al pintarse, no aparece la capa de Vite: la capa es solo para errores de transformación. React desmonta el árbol y la página se queda en blanco. En la prueba, el contenido de #root se quedó vacío. El detalle está en la consola del navegador (F12), que debe ser lo primero que mires ante una pantalla en blanco:

Error: Fallo al pintar Contador
An error occurred in the <Contador> component.

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

Los Error Boundaries que menciona el aviso son los que evitan esto en producción. Se ven en el módulo 15.

9.4. Si Fast Refresh no funciona

  • El nombre del componente empieza por minúscula. Para React y para Fast Refresh, contador no es un componente.
  • Exportas funciones anónimas. export default () => ... no le da nombre al componente. Usa una función con nombre.
  • Tu proyecto está en una carpeta sincronizada o de red, como OneDrive, o dentro de WSL con los ficheros en Windows. Puede que los cambios no se detecten. Mueve el proyecto a una carpeta local, o activa el sondeo con server: { watch: { usePolling: true } }, que consume más recursos.
  • La pestaña del navegador está conectada a un servidor antiguo. Comprueba el puerto.

10. TypeScript 7

Todo el curso usa TypeScript desde el módulo 02, y el módulo 13 profundiza en su uso con React. Aquí nos interesa como herramienta: qué hace, cuándo actúa y qué significa su configuración.

TypeScript 7.0 se publicó el 8 de julio de 2026, y es un cambio importante: el compilador se ha reescrito en Go. Hasta la 6, era un programa JavaScript que se ejecutaba sobre Node. Ahora es un binario nativo, que Microsoft cifra en compilaciones entre 8 y 12 veces más rápidas. Lo puedes ver en tu propio proyecto: dentro de node_modules/@typescript/ hay un paquete específico para tu sistema, como typescript-win32-x64.

Se usa igual que antes: la orden sigue siendo tsc, y el lenguaje no ha cambiado.

npx tsc --version
Version 7.0.2

10.1. Quién hace qué

Esta es la idea más importante de la sección, y la que más confusión genera:

  Oxc (dentro de Vite) tsc
Qué hace Quita los tipos y convierte el JSX Comprueba que los tipos son correctos
¿Mira si los tipos están bien? No Sí
¿Genera JavaScript? Sí No (noEmit: true)
Velocidad Fichero a fichero, al instante Necesita ver todo el proyecto
Cuándo actúa npm run dev y vite build Editor y npm run build

Vite no comprueba los tipos porque su transformación trata cada fichero por separado, y para comprobar tipos hay que entender todo el proyecto. Es una decisión consciente: separar las dos tareas es lo que hace que el servidor de desarrollo sea tan rápido.

Lo hemos comprobado: con un fichero lleno de errores de tipos, vite build por sí solo construyó sin protestar, mientras que tsc -b listó los cinco errores. Por eso el script build encadena las dos órdenes:

npm run build
src/fallo.ts(1,14): error TS2322: Type 'string' is not assignable to type 'number'.

Ahí se para. Vite ni siquiera llega a ejecutarse, y dist/ se queda como estaba.

💡 Consejo: Para ver los errores de tipos de todo el proyecto mientras programas, abre una segunda terminal con npx tsc -b --watch. El editor solo te enseña los de los ficheros abiertos.

10.2. Los tres tsconfig

La plantilla reparte la configuración en tres ficheros porque hay dos programas distintos con entornos distintos: el código de src/, que se ejecuta en el navegador, y vite.config.ts, que se ejecuta en Node. En el primero debe existir document, y en el segundo, process.

// tsconfig.json: solo apunta a los otros dos
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}

"files": [] significa que este fichero no compila nada por sí mismo. references es la lista de subproyectos, y por eso el script usa tsc -b (build mode), que recorre esas referencias. Un tsc sin más, en la raíz, no comprobaría nada, y no daría ningún error: es una trampa clásica.

// tsconfig.app.json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "es2023",
    "lib": ["ES2023", "DOM"],
    "module": "esnext",
    "types": ["vite/client"],
    "allowArbitraryExtensions": true,
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "erasableSyntaxOnly": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}
Opción Qué significa
tsBuildInfoFile Dónde guarda la información para compilar de forma incremental
target Versión de JavaScript que se asume. Aquí solo afecta a la comprobación
lib APIs conocidas: ES2023 para el lenguaje, DOM para document, fetch, etc.
module Sistema de módulos: import/export modernos
types Paquetes de tipos globales. vite/client aporta import.meta.env y permite importar CSS e imágenes
allowArbitraryExtensions Admite ficheros de declaración para extensiones no estándar
skipLibCheck No revisa los .d.ts de las bibliotecas. Ahorra tiempo y problemas ajenos
moduleResolution: "bundler" Resuelve los import como lo hace un empaquetador
allowImportingTsExtensions Permite escribir .tsx en los import
verbatimModuleSyntax Obliga a marcar con import type lo que solo son tipos
moduleDetection: "force" Trata cada fichero como un módulo, aunque no tenga import
noEmit No genera ficheros: de eso ya se encarga Vite
jsx: "react-jsx" Usa la transformación moderna, sin necesidad de import React
noUnusedLocals, noUnusedParameters Error si dejas variables o parámetros sin usar
erasableSyntaxOnly Prohíbe la sintaxis de TypeScript que genera código, como enum
noFallthroughCasesInSwitch Error si un case cae en el siguiente sin break

📘 Concepto: Echa de menos una opción que siempre aparece en los tutoriales: "strict": true. No está porque en TypeScript 7 es el valor por defecto. En la prueba, sin escribirla, TypeScript rechazó function saludar(nombre) con TS7006: Parameter 'nombre' implicitly has an 'any' type. También cambia types: su valor por defecto es ahora una lista vacía, y por eso la plantilla debe indicar "types": ["vite/client"].

10.3. Errores típicos que vas a ver

Hemos forzado cada uno de estos errores para ver el mensaje real:

Código Mensaje real Solución
TS1484 'Empleado' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled. import type { Empleado } from ...
TS7006 Parameter 'nombre' implicitly has an 'any' type. Anota el tipo: (nombre: string)
TS1294 This syntax is not allowed when 'erasableSyntaxOnly' is enabled. Sustituye enum por un objeto as const
TS6133 'sinUsar' is declared but its value is never read. Bórrala o úsala
TS18047 't' is possibly 'null'. Comprueba antes que no sea null

¿Por qué importa import type? Porque Oxc trata cada fichero por separado, y al ver import { Empleado } from './tipos.ts' no puede saber si Empleado es un valor o un tipo. Con import type queda claro que esa línea se borra del todo. erasableSyntaxOnly responde a una lógica parecida: el código TypeScript debe poder convertirse en JavaScript simplemente borrando los tipos. Un enum genera un objeto en ejecución, y por eso se queda fuera. La alternativa es igual de cómoda:

export const Rol = {
  Admin: 'ADMIN',
  Usuario: 'USER',
} as const

export type Rol = (typeof Rol)[keyof typeof Rol]   // 'ADMIN' | 'USER'

10.4. Opciones desaparecidas e incompatibilidades

TypeScript 7 retira opciones antiguas. Si copias un tsconfig de un tutorial, puedes encontrarte con esto:

Opción antigua Error real
"baseUrl": "." TS5102: Option 'baseUrl' has been removed.
"moduleResolution": "node" TS5108: Option 'moduleResolution=node10' has been removed.
"target": "es5" TS5108: Option 'target=ES5' has been removed.

Puedes endurecer la configuración con opciones que no vienen activadas. La más útil es noUncheckedIndexedAccess, que tiene en cuenta que lista[5] puede no existir. Sin ella, const x: string = lista[5] compila aunque la lista tenga un solo elemento. Con ella:

error TS2322: Type 'string | undefined' is not assignable to type 'string'.

La otra consecuencia de la reescritura es que TypeScript 7.0 no incluye API de JavaScript: el anuncio oficial explica que llegará una nueva en la 7.1. Las herramientas que se apoyaban en la API antigua no funcionan con él. Por eso typescript-eslint limita su versión de TypeScript a <6.1.0, y de ahí sale el error ERESOLVE de la sección 4.3. Si un proyecto necesitara esas herramientas hoy, tendría que quedarse en TypeScript 6 o usar el paquete de compatibilidad @typescript/typescript6, que este curso no utiliza.


11. Calidad del código con oxlint

Un linter lee tu código sin ejecutarlo y busca errores probables y malas prácticas. TypeScript te dice si los tipos encajan. El linter te dice cosas como “este hook está dentro de un if”, que es perfectamente válido en TypeScript y un error grave en React.

oxlint es parte del proyecto Oxc. Está escrito en Rust y sustituye a ESLint en la plantilla de Vite.

11.1. Usarlo

npm run lint

Si todo va bien, no dice nada. Para ver cómo reacciona, crea un fichero con dos errores típicos:

// src/malo.tsx
import { useState } from 'react'
export const INFO = 1
export function M({ a }: { a: boolean }) {
  if (a) {
    const [n, setN] = useState(0)
    return <p onClick={() => setN(n + 1)}>{n}</p>
  }
  return <ul>{[1, 2].map((v) => <li>{v}</li>)}</ul>
}
  x react-hooks(rules-of-hooks): React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
   ,-[src/malo.tsx:5:23]
 3 | export function M({ a }: { a: boolean }) {
 4 |   if (a) {
   :       |
   :       `-- When this condition is false, this Hook is skipped.
 5 |     const [n, setN] = useState(0)
   :                       ^^^^^|^^^^^
   :                            `-- This Hook call is not reachable on every render path.
 6 |     return <p onClick={() => setN(n + 1)}>{n}</p>
   `----
  help: Move the Hook call before the condition, or call it unconditionally and branch inside the Hook/effect instead.

  ! react(jsx-key): Missing "key" prop for element in iterator.
   ,-[src/malo.tsx:8:22]
 7 |   }
 8 |   return <ul>{[1, 2].map((v) => <li>{v}</li>)}</ul>
   :                      ^|^         ^|
   :                       |           `-- Element generated here.
   :                       `-- Iterator starts here.
 9 | }
   `----
  help: Add a "key" prop to the element in the iterator (https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key).

Found 1 warning and 1 error.
Finished in 1.2s on 4 files with 116 rules using 12 threads.

Merece la pena leer estos mensajes. Una x es un error, y !, un aviso. Las pistas explican el motivo, no solo el síntoma. Las reglas de los hooks se explican en el módulo 04, y key, en el módulo 06.

La orden devuelve un código de salida 1 si hay errores, y 0 si solo hay avisos. Eso es lo que usa un sistema de integración continua para decidir si deja pasar un cambio. Con --deny-warnings, los avisos también hacen fallar la orden.

11.2. Configurarlo

{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": ["react", "typescript", "oxc"],
  "rules": {
    "react/rules-of-hooks": "error",
    "react/only-export-components": ["warn", { "allowConstantExport": true }]
  }
}

$schema da autocompletado en el editor. plugins elige los grupos de reglas que se activan. En rules, cada regla puede ser "off", "warn" o "error", y además la plantilla usa allowConstantExport, que permite exportar constantes simples sin aviso: por eso INFO = 1 no se ha marcado arriba, y sí lo haría un objeto. Recuerda que el aviso solo te protege en parte: en la sección 9 viste que esa constante, si cambia, invalida el módulo.

La plantilla no activa todo lo que te interesa. Esta configuración añade cuatro reglas útiles:

{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": ["react", "typescript", "oxc"],
  "rules": {
    "react/rules-of-hooks": "error",
    "react/only-export-components": ["warn", { "allowConstantExport": true }],
    "react/exhaustive-deps": "warn",
    "no-console": "warn",
    "eqeqeq": "error",
    "no-var": "error"
  }
}

exhaustive-deps comprueba las dependencias de los efectos (módulo 09) y es muy recomendable. eqeqeq obliga a ===, y no-var prohíbe var. Algunas reglas se pueden corregir solas con npx oxlint --fix.

11.3. oxlint y ESLint

ESLint es el linter de referencia desde hace más de una década, y tiene un ecosistema enorme de reglas. oxlint ejecuta muchas de esas reglas ya integradas, sin configuración y mucho más rápido, pero es más joven.

  oxlint ESLint con eslint-plugin-react-hooks
Escrito en Rust JavaScript
Configuración .oxlintrc.json eslint.config.js
Reglas de hooks Sí Sí
Reglas del React Compiler En parte Sí, en el preset recommended
Con TypeScript 7 hoy Funciona typescript-eslint no lo admite (sección 10.4)

El curso usa oxlint, y volveremos a las reglas del compilador en el módulo 14.


12. Formato con Prettier

Los debates sobre comillas o puntos y comas no aportan nada. Un formateador decide por ti, y así en cada revisión de código ves cambios reales y no diferencias de espacios. La plantilla no incluye ninguno, de modo que lo instalas tú:

npm install -D prettier

Crea .prettierrc.json. La plantilla está escrita sin puntos y comas y con comillas simples. Si usas esto, la dejarás tal como está:

{
  "semi": false,
  "singleQuote": true
}

Añade los scripts a package.json:

"scripts": {
  "dev": "vite",
  "build": "tsc -b && vite build",
  "lint": "oxlint",
  "preview": "vite preview",
  "typecheck": "tsc -b",
  "format": "prettier --write .",
  "format:check": "prettier --check .",
  "check": "npm run typecheck && npm run lint && npm run format:check"
}

--write modifica los ficheros y --check solo avisa, que es lo que necesitas en integración continua. Prettier 3 ya ignora lo que esté en .gitignore, de modo que no toca node_modules ni dist.

Con el fichero de la sección 11, antes de corregirlo, esto es lo que dice:

Checking formatting...
[warn] src/malo.tsx
[warn] Code style issues found in the above file. Run Prettier with --write to fix.

📘 Concepto: Prettier y el linter no se solapan. Prettier decide el aspecto (saltos de línea, sangrado, comillas) y no sabe nada del significado. El linter busca problemas en la lógica. Si Prettier se queja, se corrige solo. Si el linter se queja, normalmente tienes que pensar.

El proyecto Oxc también tiene un formateador, oxfmt, todavía en versión 0.x. Prettier es más maduro y es el que seguimos en el curso.


13. VS Code

VS Code es el editor que usaremos, aunque todo funciona igual en WebStorm o Cursor. Conviene compartir la configuración con el proyecto, para que todo el equipo trabaje igual. Se guarda en una carpeta .vscode/, y la plantilla ya la deja preparada: su .gitignore ignora .vscode/*, salvo extensions.json.

13.1. Extensiones

Extensión Identificador Para qué
Oxc oxc.oxc-vscode Muestra los errores de oxlint en el editor
Prettier esbenp.prettier-vscode Formatea al guardar
TypeScript (Native Preview) TypeScriptTeam.native-preview Usa TypeScript 7 en el editor

La última merece una explicación. VS Code trae su propia copia de TypeScript para el editor. Cuando salió TypeScript 7, el anuncio indicaba que había que instalar esta extensión, y que el soporte integrado llegaría después. Si tu VS Code ya lo incluye, no la necesitas. Con la extensión instalada, se cambia entre ambos con las órdenes TypeScript: Enable TypeScript 7 y Disable TypeScript 7 Language Server de la paleta (Ctrl + Shift + P).

Para que VS Code sugiera las extensiones a cualquiera que abra el proyecto, crea .vscode/extensions.json:

{
  "recommendations": [
    "oxc.oxc-vscode",
    "esbenp.prettier-vscode",
    "TypeScriptTeam.native-preview"
  ]
}

13.2. Ajustes del proyecto

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.oxc": "always"
  }
}

Con esto, cada vez que guardas se formatea el fichero y se aplican las correcciones automáticas de oxlint. Para que este fichero se comparta, añade !.vscode/settings.json al .gitignore.

13.3. Depurar desde el editor

console.log te llevará lejos, pero un depurador permite parar la ejecución, ver las variables y avanzar paso a paso. Crea .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Depurar en Chrome",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}"
    }
  ]
}

Arranca npm run dev, pon un punto de ruptura en un .tsx haciendo clic junto al número de línea y pulsa F5. Chrome se abrirá y se detendrá ahí. Funciona sobre tu código original, no sobre el transformado, gracias a los mapas de código (source maps) que Vite genera en desarrollo. Si usas Edge, cambia "chrome" por "msedge".

También puedes escribir debugger en cualquier parte del código, o poner los puntos de ruptura en la pestaña Fuentes (Sources) de las herramientas del navegador.


14. React DevTools

Las herramientas del navegador te enseñan el DOM: div, button, etc. React DevTools añade una capa por encima, y te enseña lo que tú has escrito: componentes, sus props, su estado y por qué se han pintado.

14.1. Instalación

Está disponible como extensión, React Developer Tools, para Chrome, Edge y Firefox. Búscala en la tienda de tu navegador. Después, en una página hecha con React, aparecen dos o tres pestañas nuevas en F12, con el símbolo de React delante de cada nombre.

El icono de la extensión indica cómo está construida la página en la que estás: si no usa React, sale apagado, y distingue entre versión de desarrollo y de producción. Pruébalo en http://localhost:5173 y en cualquier web pública hecha con React.

Para Safari u otros navegadores existe una versión independiente:

npm install -g react-devtools
react-devtools

Esa aplicación te dará una línea <script src="http://localhost:8097"></script>, que debes añadir al principio del <head>. Quítala antes de publicar.

14.2. Inspeccionar componentes

Aplicación EMPRESA arriba, con el botón Contador resaltado, y debajo React DevTools con el árbol App, Cabecera y Contador, y el panel de detalle con State igual a 3

Figura 11. Interfaz real de React DevTools 8 sobre el contador. Se ha capturado con la versión integrable de las DevTools (react-devtools-inline), que necesita una compilación experimental de React, y por eso la línea rendered by no indica 19.3.1. La extensión muestra lo mismo.

La pestaña Components tiene dos zonas. A la izquierda, el árbol de componentes, que sigue tu jerarquía, no la del DOM. A la derecha, los detalles del seleccionado:

Sección Qué muestra
props Lo que recibe el componente. Se puede editar en directo
hooks Sus hooks y sus valores. Aquí State: 3 es el useState del contador
rendered by Quién lo ha pintado, hasta la raíz, con el enlace al código
source Fichero y línea, Contador.tsx:5

Haz esta prueba: selecciona Contador, haz clic en el valor de State, escribe 100 y pulsa Intro. La interfaz se actualiza como si hubieras pulsado el botón cien veces. Sirve para probar casos difíciles de reproducir, como una lista de 1000 elementos, sin tocar el código.

Otros trucos útiles:

  • Seleccionar en la página: el icono de la flecha, arriba a la izquierda, te deja hacer clic en un elemento y saltar a su componente.
  • Buscar: el cuadro de búsqueda filtra por nombre, y acepta expresiones regulares.
  • Ver el DOM: el icono del ojo salta a la pestaña Elementos con el nodo real del componente.
  • $r en la consola: con un componente seleccionado, $r en la consola te da acceso a él.
  • Resaltar actualizaciones: en la rueda dentada, la opción Highlight updates when components render dibuja un borde en cada componente que se vuelve a pintar. Es la forma más rápida de descubrir repintados inesperados.

14.3. Las otras pestañas

  • Profiler graba una interacción y te dice qué componentes se han pintado, cuántas veces y cuánto han tardado. Se estudia en el módulo 14.
  • Suspense muestra los límites de carga de la aplicación. Tiene más sentido cuando conozcas Suspense, en el módulo 15.

15. Construir y probar la versión de producción

15.1. npm run build

npm run build

Esta es la salida de la plantilla sin tocar:

> mi-app@0.0.0 build
> tsc -b && vite build

vite v8.3.5 building client environment for production...
transforming...
✓ 20 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                   0.45 kB │ gzip:  0.29 kB
dist/assets/react-CHdo91hT.svg    4.12 kB │ gzip:  2.06 kB
dist/assets/vite-BF8QNONU.svg     8.70 kB │ gzip:  1.60 kB
dist/assets/hero-CLDdwZDr.png    13.05 kB
dist/assets/index-D64VDMd1.css    4.10 kB │ gzip:  1.47 kB
dist/assets/index-GpEN3EET.js   222.44 kB │ gzip: 69.28 kB

✓ built in 4.52s

Los 20 módulos caben en un único JavaScript. La columna gzip es lo que importa de verdad: es lo que viaja por la red cuando el servidor comprime, que es lo normal. Unos 69 kB para tener React es un punto de partida razonable, y casi todo es React y React DOM.

Compara index.html antes y después:

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>EMPRESA</title>
    <script type="module" crossorigin src="/assets/index-vZQ33M9I.js"></script>
    <link rel="stylesheet" crossorigin href="/assets/index-AA0y7VqQ.css">
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Vite lo ha reescrito: /src/main.tsx ya no está, y en su lugar aparecen los ficheros finales, con huella.

15.2. npm run preview

No abras dist/index.html con doble clic. Las rutas son absolutas, empiezan por /, y los módulos no se cargan desde file://. Verías una página en blanco. Necesitas un servidor:

npm run preview
  ➜  Local:   http://localhost:4173/
  ➜  Network: use --host to expose

Esta orden sirve el contenido de dist/ tal como está: observa que el puerto es otro, y que el texto de EMPRESA dice production, y ya no development. Es la mejor forma de comprobar el resultado antes de publicarlo.

⚠️ Importante: preview sirve para probar en local. No es un servidor de producción. Para publicar la aplicación, sube dist/ a un servicio de ficheros estáticos o a tu servidor, como se ve en el módulo 25.

15.3. Publicar en una subcarpeta: base

Por defecto, Vite asume que la aplicación se publica en la raíz del dominio. Si la publicas en https://usuario.github.io/empresa/, el navegador buscará /assets/index-xxx.js en la raíz y no lo encontrará. El síntoma es muy conocido: página en blanco y errores 404 en la consola. Se resuelve con base:

export default defineConfig({
  base: '/empresa/',
  plugins: [react()],
})

Las rutas cambian en el HTML generado:

<link rel="icon" type="image/svg+xml" href="/empresa/favicon.svg" />
<script type="module" crossorigin src="/empresa/assets/index-DLl5HmH4.js"></script>
<link rel="stylesheet" crossorigin href="/empresa/assets/index-AA0y7VqQ.css">

Y preview respeta ese prefijo: la dirección pasa a ser http://localhost:4173/empresa/, y si pides /, te redirige allí.

⚠️ Importante: Escribe base en vite.config.ts. Si lo pasas por la línea de órdenes desde Git Bash (--base=/empresa/), MSYS convierte lo que parece una ruta de Unix en una de Windows, y obtienes esto de verdad en el HTML: src="/Program Files/Git/empresa/assets/...". Si necesitas hacerlo así, antepón MSYS_NO_PATHCONV=1.

preview y el servidor de desarrollo tienen otro comportamiento que conviene recordar: ante una ruta desconocida como /empresa/ruta/x devuelven index.html, y no un 404. Es lo que necesita una SPA con rutas en el cliente (módulo 18), pero tu servidor real no lo hará si no lo configuras.

15.4. Dividir el código

Una aplicación grande no debería descargar todo de golpe. Si importas un módulo con la función import(), en vez de la sentencia import, Rolldown lo separa en otro fichero, que solo se descarga cuando se ejecuta esa línea:

const { generarInforme } = await import('./informe.ts')

Así lo hemos comprobado: el módulo apareció como fichero independiente.

dist/assets/informe-BxRcRcFx.js    0.09 kB │ gzip:  0.10 kB
dist/assets/index-Zdzg0seU.js    221.95 kB │ gzip: 69.59 kB

Con componentes se usa lazy, que verás en el módulo 15.


16. Instalar otra versión de React

React no tiene versiones LTS, como se explica en el módulo 00. La versión que usas es la que hayas instalado, y en algunos proyectos no podrás usar la última. En este curso, cada apartado que depende de una versión concreta lleva una marca 🆕 o 🔄, con la alternativa para versiones anteriores.

Este módulo no lleva ninguna marca porque todo lo que cuenta funciona igual: lo hemos comprobado construyendo la plantilla con React 18.3.1 y con 19.1.10.

Tu caso Orden
Última versión npm install react@latest react-dom@latest
Una versión exacta npm install -E react@19.1.10 react-dom@19.1.10
La línea con parches de la etiqueta backport npm install -E react@backport react-dom@backport
React 18 npm install -E react@18.3.1 react-dom@18.3.1 y npm install -D @types/react@18 @types/react-dom@18

Si bajas de React 19 a 18, los paquetes @types tienen que bajar también. Y react y react-dom deben ir siempre en la misma versión.

⚠️ Importante: Usa -E. Sin esa opción, npm install react@backport deja en package.json el rango ^19.1.10. Te ha instalado 19.1.10, pero ese rango también admite la 19.3.1, de modo que el siguiente npm update te subiría de línea sin avisar. Con -E queda "react": "19.1.10".

Comprueba siempre qué tienes instalado, porque lo que dice package.json es un rango:

npm ls react react-dom
mi-app@0.0.0
├─┬ react-dom@19.1.10
│ └── react@19.1.10 deduped
└── react@19.1.10

Por curiosidad, esto es lo que ocupa el JavaScript de la misma plantilla con cada versión:

React index-*.js gzip
18.3.1 143,51 kB 46,39 kB
19.1.10 185,97 kB 58,68 kB
19.3.1 222,44 kB 69,28 kB

Algunas bibliotecas ponen un mínimo. React Router 8, por ejemplo, exige react >= 19.2.7, así que con versiones anteriores no podrías seguir el módulo 18 tal como está escrito.


17. Problemas frecuentes

Síntoma Causa probable Solución
Aparece src/main.ts y no hay React PowerShell se ha comido el -- Usa "--" o Git Bash (sección 4.2)
npm error code ERESOLVE al instalar Elegiste la opción ESLint Repite con Oxlint (sección 4.3)
npm.ps1 cannot be loaded Política de ejecución de PowerShell Sección 2.3
Vite dice que necesita otra versión de Node Node demasiado antiguo Instala una LTS actual
Cannot find native binding node_modules copiado de otro sistema o instalación incompleta Borra node_modules y el lock, y reinstala
Página en blanco en desarrollo Excepción al pintar Mira la consola (F12)
Página en blanco tras publicar Falta base Sección 15.3
Página en blanco al abrir dist/index.html No se puede abrir con doble clic Usa npm run preview
import.meta.env.X es undefined Falta el prefijo VITE_, o el servidor no se ha reiniciado Sección 8.7
Compila en dev y falla en build Vite no comprueba tipos Ejecuta npx tsc -b y corrige
Failed to fetch hacia /api La API no está arrancada o el proxy está mal Prueba la URL de la API directamente
Los cambios no aparecen Otra pestaña, otro puerto u OneDrive Sección 9.4
Option 'baseUrl' has been removed tsconfig copiado de un tutorial antiguo Quita baseUrl
El estado se reinicia al guardar Cambia la firma de hooks o hay exportaciones mezcladas Sección 9
tsc no da errores, pero el build sí Falta -b: un tsc solo en la raíz no comprueba nada Usa tsc -b

18. Ejercicios

Ejercicio 1: Tu proyecto EMPRESA

Crea un proyecto empresa-web con la plantilla react-ts y déjalo como el de la sección 7, con lang="es", el título EMPRESA, Cabecera, Contador y el CSS. Después comprueba que npm run lint y npm run build terminan sin errores, y que npm run preview muestra production.

Ver solución
npm create vite@latest empresa-web -- --template react-ts   # en PowerShell: "--"
cd empresa-web
npm install

Haz los cambios de la sección 7, y borra src/App.css y src/assets/hero.png. Si borras App.css y App.tsx sigue importándolo, la compilación fallará, porque el fichero ya no existe. Sustituir App.tsx por el de la sección 7 elimina esa importación.

npm run lint
npm run build
npm run preview

En el navegador, el texto debe pasar de Modo: development, con npm run dev, a Modo: production, con npm run preview. Eso demuestra que import.meta.env.MODE se ha sustituido al construir.

Ejercicio 2: Un script que lo compruebe todo

Instala Prettier y añade a package.json cuatro scripts: typecheck, format, format:check y check. Este último debe comprobar tipos, lint y formato, y debe detenerse en el primer fallo. Introduce después un error de tipos y comprueba que npm run check no llega a ejecutar el linter.

Ver solución
npm install -D prettier
{
  "semi": false,
  "singleQuote": true
}
"typecheck": "tsc -b",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "npm run typecheck && npm run lint && npm run format:check"

El && es el que detiene la cadena: cada orden devuelve un código de salida, y la siguiente solo se ejecuta si ese código es 0. Es lo mismo que hace el script build con tsc -b && vite build.

Salida con el proyecto correcto:

> mi-app@0.0.0 format:check
> prettier --check .

Checking formatting...
All matched files use Prettier code style!

Es solo el final de la salida. Antes aparecen los bloques de typecheck y lint, que no imprimen nada si todo está bien.

Ejercicio 3: Variables de entorno tipadas

Crea un .env con VITE_NOMBRE_APP=EMPRESA y CLAVE_SECRETA=algo. Haz que App tome el título de la cabecera de la variable. Declara la variable en src/env.d.ts de forma que TypeScript rechace una errata como import.meta.env.VITE_NOMBRE. Por último, muestra import.meta.env.CLAVE_SECRETA y explica qué pasa.

Ver solución
// src/env.d.ts
interface ViteTypeOptions {
  strictImportMetaEnv: unknown
}

interface ImportMetaEnv {
  readonly VITE_NOMBRE_APP: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}
<Cabecera titulo={import.meta.env.VITE_NOMBRE_APP} />

Con la declaración, VITE_NOMBRE_APP es string y no any, así que titulo recibe el tipo correcto. Con strictImportMetaEnv, la errata da TS2339. Sin esa opción compilaría, porque los tipos de vite/client admiten cualquier clave.

Con esa misma opción, import.meta.env.CLAVE_SECRETA también da TS2339, y es correcto. Quítala un momento para ver qué ocurre en ejecución: el valor es undefined, porque la variable no empieza por VITE_. Vite la deja fuera a propósito, y el valor no llega a dist/.

Ejercicio 4: Alias @

Configura el alias @ para src/, cambia las importaciones de App.tsx y comprueba que funcionan tanto npm run dev como npm run build. Prueba después a quitar solo la parte de Vite y explica qué sucede.

Ver solución
// tsconfig.app.json, dentro de compilerOptions
"paths": { "@/*": ["./src/*"] },
// vite.config.ts
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
  resolve: {
    tsconfigPaths: true,
  },
})
// src/App.tsx
import Cabecera from '@/componentes/Cabecera.tsx'
import Contador from '@/componentes/Contador.tsx'

Si quitas resolve.tsconfigPaths, el resultado es curioso, y te hará perder el tiempo si no lo conoces:

Orden Resultado sin tsconfigPaths
tsc -b Pasa, porque TypeScript conoce paths
vite build Pasa también: Rolldown leyó la configuración de TypeScript en nuestra prueba
npm run dev Falla: Failed to resolve import "@/componentes/Cabecera.tsx" from "src/App.tsx"

Es decir, el servidor de desarrollo no lee paths por su cuenta, aunque la construcción sí lo haya hecho. tsc, el servidor y el empaquetador son programas distintos, y cada uno resuelve las rutas a su manera. Con tsconfigPaths: true los tres coinciden.

Ejercicio 5: Departamentos a través del proxy

Arranca la API EMPRESA, configura el proxy de /api, y añade a tu proyecto un componente ListaDepartamentos que reciba por props la lista y la pinte. Como aún no conoces useEffect, haz la petición en main.tsx, antes de pintar, usando await en el nivel superior del módulo. Los datos vienen en la propiedad contenido, y cada departamento tiene deptId, nombre y localidad.

Ver solución
// src/componentes/ListaDepartamentos.tsx
interface Departamento {
  deptId: number
  nombre: string
  localidad: string
}

function ListaDepartamentos({ departamentos }: { departamentos: Departamento[] }) {
  return (
    <main>
      <h1>Departamentos</h1>
      <ul>
        {departamentos.map((d) => (
          <li key={d.deptId}>
            {d.nombre} ({d.localidad})
          </li>
        ))}
      </ul>
    </main>
  )
}

export default ListaDepartamentos
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import ListaDepartamentos from '@/componentes/ListaDepartamentos.tsx'

const respuesta = await fetch(`${import.meta.env.VITE_API_URL}/departamentos`)
const pagina = await respuesta.json()

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <ListaDepartamentos departamentos={pagina.contenido} />
  </StrictMode>,
)

Lista de departamentos de EMPRESA con DIRECCIÓN, VENTAS y el resto, leídos de la API

Figura 12. Los datos llegan de la API en el puerto 3000 a través del proxy de Vite.

Es una solución de ejercicio, no un patrón para aplicaciones reales. La página no pinta nada hasta que responde la API, y si la API falla, se queda en blanco. Lo correcto se verá en el módulo 09 y en el módulo 19.

Ejercicio 6: Cuatro errores de TypeScript

Corrige este código sin desactivar ninguna opción del tsconfig:

// src/dominio/tipos.ts
export interface Empleado {
  id: number
  nombre: string
}

export enum Rol {
  Admin = 'ADMIN',
  Usuario = 'USER',
}
// src/dominio/fichas.ts
import { Empleado, Rol } from './tipos.ts'

export function etiquetar(empleado: Empleado, rol) {
  const sinUsar = 1
  return `${empleado.nombre.toUpperCase()} (${rol})`
}

export const ejemplo = etiquetar({ id: 100, nombre: 'Ana' }, Rol.Admin)
Ver solución
// src/dominio/tipos.ts
export interface Empleado {
  id: number
  nombre: string
}

export const Rol = {
  Admin: 'ADMIN',
  Usuario: 'USER',
} as const

export type Rol = (typeof Rol)[keyof typeof Rol]
// src/dominio/fichas.ts
import { Rol } from './tipos.ts'
import type { Empleado } from './tipos.ts'

export function etiquetar(empleado: Empleado, rol: Rol): string {
  return `${empleado.nombre.toUpperCase()} (${rol})`
}

export const ejemplo = etiquetar({ id: 100, nombre: 'Ana' }, Rol.Admin)

Hay cuatro correcciones. enum no está permitido por erasableSyntaxOnly, así que pasa a ser un objeto as const, con un tipo del mismo nombre (son espacios separados: uno existe en ejecución y el otro, solo en compilación). Empleado es un tipo, y verbatimModuleSyntax exige import type. rol necesita un tipo, porque strict prohíbe el any implícito. Y sinUsar se borra por noUnusedLocals.

Rol se importa sin type porque aquí es un valor (Rol.Admin) además de un tipo. El llamador sigue escribiendo Rol.Admin, igual que con el enum.

Ejercicio 7: Un componente que no pasa el linter

Añade react/exhaustive-deps, no-console, eqeqeq y no-var a .oxlintrc.json. Después arregla los problemas que señala oxlint en este componente, sin usar comentarios que desactiven reglas:

import { useState } from 'react'

export function Lista({ activo }: { activo: boolean }) {
  if (activo) {
    const [veces, setVeces] = useState(0)
    return <p onClick={() => setVeces(veces + 1)}>{veces}</p>
  }

  var valores = [1, 2]
  if (valores.length == 2) console.log('dos')

  return <ul>{valores.map((n) => <li>{n}</li>)}</ul>
}
Ver solución
import { useState } from 'react'

export function Lista({ activo }: { activo: boolean }) {
  const [veces, setVeces] = useState(0)

  if (activo) {
    return <p onClick={() => setVeces(veces + 1)}>{veces}</p>
  }

  return (
    <ul>
      {[1, 2].map((n) => (
        <li key={n}>{n}</li>
      ))}
    </ul>
  )
}
  • useState sube al principio de la función: un hook no puede estar dentro de un if, porque React identifica cada hook por su orden de llamada.
  • Cada <li> lleva key.
  • Desaparecen el var, el == y el console.log.

Ejercicio 8: Predice qué hará Fast Refresh

Con la aplicación arrancada y el contador en 3, predice qué ocurre en cada caso. Compruébalo después:

  1. Cambias el color del botón en index.css.
  2. Cambias useState(0) por useState(5).
  3. Añades un <p> en Cabecera.tsx.
  4. Añades export const LIMITE = 5 en Contador.tsx.
  5. Añades export const LIMITE = 5 en App.tsx.
  6. Añades un comentario en main.tsx.
Ver solución
Caso Contador ¿Recarga? Por qué
1 3 No El CSS se sustituye sin tocar React
2 5 No Cambia la firma de los hooks, y se remonta Contador
3 3 No Cabecera no es Contador, y Fast Refresh solo afecta a lo editado
4 3 No Se invalida el módulo, pero App.tsx recoge la actualización
5 0 Sí Se invalida App.tsx, y main.tsx no acepta actualizaciones
6 0 Sí main.tsx es el punto de entrada: nadie por encima lo acepta

En los casos 4 y 5 aparece hmr invalidate en la terminal, y en el 5 y el 6, page reload.

Ejercicio 9: Publicar en una subcarpeta

Configura el proyecto para publicarlo en /empresa/. Construye y responde: ¿qué dirección debes abrir con preview? ¿Qué ocurre si pides /? ¿Y una ruta inventada, como /empresa/ruta/x? ¿Y un fichero inexistente en /assets/x.js?

Ver solución
export default defineConfig({
  base: '/empresa/',
  plugins: [react()],
})

Tras npm run build y npm run preview, la dirección es http://localhost:4173/empresa/. Estas son las respuestas reales, pedidas con curl:

Petición Respuesta Comentario
/empresa/ 200, HTML La aplicación
/ 302 Redirige a /empresa/
/empresa/ruta/x 200, HTML Fallback de SPA: entrega index.html
/assets/x.js 404 Fuera de base, con un mensaje que sugiere la ruta correcta

Ese 200 en una ruta inventada es lo que permitirá usar rutas en el cliente. Un servidor real necesitará esa misma regla.

Ejercicio 10: Separar un módulo pesado

Crea src/informe.ts con una función generarInforme(), y un componente BotonInforme que cargue ese módulo solo al pulsarlo y muestre el resultado con alert. Construye y comprueba que aparece un fichero aparte. Después, abre la pestaña Red con preview y comprueba cuándo se descarga.

Ver solución
// src/informe.ts
export function generarInforme(): string {
  return 'Informe generado: ' + new Date().getFullYear()
}
// src/componentes/BotonInforme.tsx
function BotonInforme() {
  async function pulsar() {
    const { generarInforme } = await import('../informe.ts')
    alert(generarInforme())
  }

  return (
    <button type="button" onClick={pulsar}>
      Generar informe
    </button>
  )
}

export default BotonInforme

Al construir aparece dist/assets/informe-xxxx.js. En la pestaña Red, esa petición no sale al cargar la página, sino en la primera pulsación. En las siguientes ya no hay petición, porque el navegador conserva los módulos cargados.

Con un módulo tan pequeño no tiene sentido. Compensa con bibliotecas pesadas que solo usan algunos usuarios, como un generador de PDF o un editor de gráficos.

Ejercicio 11 (propuesto): Configuración compartida del editor

Prepara .vscode/extensions.json, .vscode/settings.json y .vscode/launch.json para tu proyecto. Ajusta .gitignore para que los tres se suban a Git. Clónalo en otra carpeta y comprueba que VS Code te sugiere las extensiones y que al guardar se aplica el formato. Pon un punto de ruptura en Contador y comprueba que la ejecución se detiene en cada pulsación.

Ejercicio 12 (propuesto): Comparar versiones de React

Haz una copia del proyecto y bájalo a React 18.3.1, incluidos sus tipos. Comprueba con npm ls lo que tienes instalado, y compara el tamaño del JavaScript con el de la tabla de la sección 16. Después intenta añadir useActionState (que es de la versión 19.0) en un componente y lee el error que da TypeScript. Con eso entenderás para qué sirven las marcas 🆕 del curso.


Resumen

Concepto API Idea clave
Node.js node -v Ejecuta las herramientas, no tu aplicación. Usa una LTS
Dependencias package.json, package-lock.json Rangos frente a versiones exactas. El lock va a Git, node_modules no
Instalación reproducible npm ci Instala exactamente lo que dice el lock
Crear un proyecto npm create vite@latest mi-app -- --template react-ts En PowerShell, "--". Elige Oxlint
Punto de entrada index.html, main.tsx createRoot(...).render(...) sobre #root
Recursos src/assets/, public/ Lo importado lleva huella. public/ se copia tal cual
Desarrollo npm run dev Módulos sueltos transformados por Oxc, sin comprobar tipos
Proxy server.proxy Evita CORS en desarrollo. No existe en producción
Alias paths, resolve.tsconfigPaths @/ se configura en TypeScript y en Vite
Variables de entorno import.meta.env.VITE_* Se incrustan al construir. Son públicas
Fast Refresh Automático Conserva el estado si los hooks no cambian y el fichero solo exporta componentes
Comprobar tipos tsc -b Vite no lo hace. npm run build sí, y se detiene si hay errores
TypeScript 7 tsconfig.app.json Nativo en Go. strict por defecto. Sin baseUrl
Sin enum as const erasableSyntaxOnly: el TS debe poder borrarse
Lint npm run lint, .oxlintrc.json Reglas de hooks y key. exhaustive-deps se activa a mano
Formato prettier --write . Aspecto, no lógica. Se ejecuta al guardar
Depuración React DevTools, F12 Árbol de componentes, props, hooks y rendered by
Producción npm run build, npm run preview Rolldown genera dist/. Nunca lo abras con doble clic
Subcarpeta base Sin ella, página en blanco y 404
División de código import() Crea un fichero aparte, descargado bajo demanda
Otra versión de React npm install -E react@x react-dom@x Siempre con -E y las dos a la vez