Apariencia
📙 Clase 15 — Crear un proyecto React con Vite y TypeScript
TypeScript · 2026-08-06 (ampliada 2026-08-07) · Carpeta:
02-Ejercicios/React/plantillaPlatzi⬅️ Volver al índice de clases
🎯 Qué aprendí
- Qué es Vite y por qué genera proyectos completos (React + TypeScript) en un solo comando, sin configurar nada a mano.
- Qué es JSX y por qué los archivos de React llevan extensión
.tsxen vez de.ts. - El flujo completo y exacto que seguí en el asistente:
npm create vite@latest→ nombre del proyecto → nombre del paquete → framework → variante →npm install→npm run dev. - Cómo está organizada la plantilla:
src/(código),public/(estáticos),App.tsx(componente raíz),main.tsx(punto de entrada que monta la app). - La cadena de
tsconfig.json→tsconfig.app.json+tsconfig.node.json, y por qué elbuildusatsc -ben vez detsca secas. - Un hallazgo real importante:
npm run devNO revisa tipos — solonpm run buildlo hace. - 🐛 Un bug real que me pasó de verdad, no solo leí sobre él: al elegir la variante equivocada del asistente (pensando en la opción "TypeScript + SWC" del material que estudié), terminé con React Compiler sobre Vite 8 (Rolldown-Vite, experimental) — una combinación que rompe el HMR con el error
RefreshRuntime.register is not a functiony deja la pantalla en negro. Diagnostiqué la causa y verifiqué la solución (ver sección 9 de la teoría). - 🎨 Un segundo incidente real, ya con el proyecto arreglado: edité
App.cssa mano y borré sin querer 5 líneas — descuadré las llaves{ }y VS Code mostró "9+" errores en cascada. Aprendí a diferenciar un error real de un falso positivo del editor: conté las llaves conawkpara confirmar que el archivo estaba bien formado, y descubrí que el validador CSS integrado de VS Code no reconoce el anidamiento nativo sin&(ver sección 9.1 de la teoría). - 🧩 El proyecto creció en un caso práctico real, sin crear proyectos nuevos: desde el ejercicio 13,
plantillaPlatzideja de ser una plantilla de demostración y se convierte en Nexus, un panel de empleados que consume la API real de la Clase 17 —fetchcon estado de carga/error, formularios controlados con CRUD completo, autenticación JWT (registro, login, token enlocalStorage, headersAuthorization), búsqueda/orden/paginación por query params, y un panel de estadísticas con caché. Cada pieza, verificada contra la base de datos SQLite real, no simulada (sección 11).
📚 Definiciones clave
| Término | Qué es | Ejemplo de esta clase |
|---|---|---|
| Vite | Herramienta que genera la plantilla de un proyecto frontend completo | npm create vite@latest |
JSX / .tsx | Sintaxis tipo HTML dentro de JavaScript/TypeScript; React la usa de forma nativa | <button>Contador {count}</button> |
App.tsx | Componente raíz de React — lo que ves en el navegador sale de aquí | src/App.tsx |
tsc -b | "Build mode" de TypeScript: compila un proyecto que usa references entre varios tsconfig.json | "build": "tsc -b && vite build" |
| HMR / Fast Refresh | Actualiza el navegador sin recargar la página, preservando el estado de React | edito App.tsx, el contador no se reinicia |
📖 PARTE TEÓRICA
⚡ 1. Qué es Vite y por qué se usa con React + TypeScript
Vite genera una plantilla de proyecto ya construida, para no perder tiempo configurando compilador, paquetes y estructura a mano. Combinado con React, te da archivos .tsx en vez de .ts — la extensión que habilita JSX dentro de TypeScript.
🧪 Entrevista: ¿Qué es JSX? Una sintaxis tipo HTML dentro de archivos JavaScript/TypeScript. React la usa de forma nativa; por eso sus componentes viven en archivos
.tsx(o.jsxen JavaScript puro) en vez de.ts.
🎯 2. Relaciona las columnas: los 4 comandos base
Antes de entrar al detalle, el resumen de los cuatro comandos que arman todo el flujo de esta clase — cada uno con una única función, en el orden en que normalmente los usas:

| Comando | Qué hace |
|---|---|
npm create vite@latest | Genera la estructura inicial del proyecto con plantilla |
npm install | Descarga los paquetes que el proyecto requiere |
tsc | Compila los archivos TypeScript del proyecto |
npm run dev | Inicia el servidor local para previsualizar la app |
💡 Fíjate en el orden: primero generas la estructura, luego descargas dependencias — nunca al revés, porque
npm installlee elpackage.jsonquenpm create vite@latestacaba de crear.tscynpm run devvienen después, cuando ya tienes código para compilar o previsualizar.
🧙 3. El asistente, paso a paso — con los prompts EXACTOS
Parado en la carpeta del proyecto (02-Ejercicios/React/):
bash
npm create vite@latestEstos son los pasos reales, en orden, tal como los preguntó el asistente (create-vite@9.1.2 en este equipo):
Project name:
plantillaPlatzi— el nombre de la carpeta que crea.Package name:
plantillaplatzi— se sugiere solo, en minúsculas, parapackage.json.Select a framework: una lista larga —
Vanilla · Vue · React · Preact · Lit · Svelte · Solid · Ember · Qwik · Angular · Marko— elegí React.Select a variant: dentro de React, la lista real (revisé el código fuente del asistente para confirmar el orden exacto):
# Opción mostrada Nombre interno ¿La elegí? 1 TypeScript react-ts2 TypeScript + React Compiler react-compiler-ts✅ sí — el material decía "opción 2 = SWC", pero hoy la opción 2 cambió de nombre 3 JavaScript react4 JavaScript + React Compiler react-compiler5+ RSC, React Router, TanStack Router, RedwoodSDK… (starters externos) —
⚠️ Aquí ocurrió el error real de esta clase. Elegí la opción 2, "TypeScript + React Compiler" — y lo hice por qué: el material que estaba estudiando decía "Elegir la variante TypeScript + SWC, que es la opción número dos". Ese consejo tenía sentido en una versión anterior del asistente, donde la opción 2 sí era "TypeScript + SWC". Seguí la posición de la lista al pie de la letra, sin fijarme que el nombre de esa opción había cambiado con el tiempo — hoy la opción 2 ya no es SWC, es React Compiler, algo completamente distinto. Esa elección (React Compiler
- la versión de Vite que se instaló en ese momento) causó el bug real que diagnostico en la sección 9. La lección: en un asistente de línea de comandos, lee el nombre completo de la opción, no confíes en que la posición se mantenga igual entre versiones.
Qué significa cada opción de la tabla:
| Opción | Qué instala en realidad | Ejemplo de cuándo la elegirías |
|---|---|---|
⭐ TypeScript (react-ts) — la recomendada | React + TypeScript "a secas": @vitejs/plugin-react (transforma JSX con Babel, el motor clásico y estable). Sin memoización automática — si quieres optimizar renders, escribes useMemo/useCallback tú mismo. Es la opción recomendada para aprender y para la mayoría de proyectos. | Cualquier proyecto normal de aprendizaje o producción — es la opción con la que debería haber creado plantillaPlatzi desde el principio. |
TypeScript + React Compiler (react-compiler-ts) — la que elegí | Lo mismo que la anterior, más el React Compiler: un plugin de Babel (babel-plugin-react-compiler) que analiza tu código y agrega memoización automática — el trabajo que antes hacías a mano. Es una pieza relativamente nueva del ecosistema React, todavía estabilizándose junto con el resto de las herramientas (de ahí el bug de la sección 9). | Una app con muchos componentes que se re-renderizan seguido (listas grandes, formularios complejos, dashboards) donde no quieres escribir useMemo/useCallback a mano en cada uno — a cambio de aceptar una herramienta más nueva y, como viste, todavía con fricciones. |
JavaScript (react) | Igual que "TypeScript", pero sin TypeScript — archivos .jsx en vez de .tsx, sin tipado ni tsc en el build. | Un prototipo rápido y descartable, o un equipo/proyecto que decidió explícitamente no usar TypeScript. |
JavaScript + React Compiler (react-compiler) | La versión JavaScript de la opción que elegí — mismo React Compiler, sin tipado. | El mismo caso que la anterior (sin TypeScript), pero además quieres la memoización automática — poco común, ya que quien elige React Compiler suele venir de un proyecto tipado. |
Las siguientes no son "variantes" del mismo proyecto simple — son plantillas de frameworks completos, cada una con su propio comando de instalación (customCommand en el código del asistente) en vez de ser solo una plantilla de Vite. Van más allá del alcance de esta clase, pero vale la pena saber para qué serviría cada una si algún día las necesitas:
| Starter | Qué es | Ejemplo de cuándo lo elegirías |
|---|---|---|
| RSC (React Server Components) | Componentes que se renderizan en el servidor por defecto, no en el navegador — pueden leer datos directamente (de una base de datos, un archivo) sin pasar por una API ni por fetch en el cliente. | Un dashboard que muestra datos de una base de datos: en vez de crear un endpoint /api/usuarios y hacer fetch desde React, el componente del servidor consulta la base de datos directamente y envía solo el HTML resultante al navegador. |
| React Router v7 | Framework de ruteo completo (con SSR, y loader/action para cargar y enviar datos por ruta) — el sucesor de Remix, ahora integrado en React Router. | Una tienda en línea con varias páginas (/productos, /productos/:id, /carrito, /checkout), donde cada ruta necesita cargar sus propios datos del servidor antes de renderizar, con URLs que funcionan bien para SEO. |
| TanStack Router | Router con tipado de extremo a extremo: los parámetros de ruta y los query params autocompletan y se revisan con TypeScript, no son solo texto. | Un panel de administración con filtros complejos en la URL (?pagina=2&orden=fecha&estado=activo) donde quieres que TypeScript te avise si escribes mal un parámetro, en vez de descubrirlo en producción. |
| RedwoodSDK | Framework full-stack (React + funciones de servidor + base de datos + autenticación) pensado para desplegar en el edge (Cloudflare Workers). | Una app completa con usuarios que inician sesión y guardan datos (por ejemplo, una lista de tareas con cuentas), sin querer armar un backend por separado — todo vive en el mismo proyecto. |
📦 4. Instalar y correr el servidor de desarrollo
bash
cd plantillaPlatzi
npm install
npm run devVerificado en tu proyecto real (ya con la configuración corregida, sección 9): npm install instaló 142 paquetes — muy cerca de los ~141 que menciona el material original.
💡 El número exacto de paquetes no es un dato fijo: depende de qué versiones estén publicadas el día que corras el comando, y de qué variante hayas elegido (React Compiler agrega paquetes de Babel extra). No te alarmes si tu número no coincide exactamente con el mío — lo importante es que
npm installtermine sin errores.
npm run dev levanta el servidor y te devuelve una URL con el puerto:
$ npm run dev
VITE v7.3.6 ready in 118 ms
➜ Local: http://localhost:5173/🗂️ 5. Estructura del proyecto: src/, public/, App.tsx
plantillaPlatzi/
├── public/ ← estáticos (favicon, iconos)
├── src/
│ ├── main.tsx ← punto de entrada: monta <App /> en el HTML
│ ├── App.tsx ← componente raíz: lo que ves en el navegador
│ ├── App.css
│ └── assets/
├── index.html ← el único HTML de todo el proyecto
├── package.json
├── vite.config.ts
├── tsconfig.json
├── tsconfig.app.json
└── tsconfig.node.jsonindex.html solo monta un <div id="root"> vacío y carga main.tsx como módulo:
html
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>main.tsx es quien conecta React con ese <div>:
typescript
// src/main.tsx
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(<App />)📝 El
!después degetElementById('root')es el operador de aserción no-nula que ya viste en la Clase 2: "confío en que este elemento existe, no verifiques que seanull". Distinto al?.de la Clase 14, que sí verifica antes de acceder.
App.tsx es el componente que realmente editas cuando cambias algo visible en pantalla. En mi proyecto, cambié el texto del botón de Count is {count} a Contador {count} (línea 29):
⚠️ También cambió respecto al material original: el video describe una plantilla minimalista con el texto en la línea 22. La plantilla real instalada hoy es una landing page completa ("Get started", "Documentation", "Connect with us"), y el botón del contador está en la línea 29, no la 22. El concepto — "edita
App.tsx, guarda, se actualiza solo" — sigue siendo el mismo; solo cambió el contenido de la plantilla por defecto.
🔁 6. HMR: edita y guarda, se actualiza sin recargar
Verificado en tu proyecto real (ya corregido): cambié Contador {count} a Contador: {count}, guardé, y sin recargar el navegador el texto cambió solo:
[vite] (client) hmr update /src/App.tsx💡 Antes de editar, hice clic en el botón (contador en 1). Después del HMR, el texto decía
Contador: 1— no se reinició a 0. React (con Vite) preserva el estado del componente entre actualizaciones de HMR, no solo el texto visible.
⚙️ 7. tsconfig.json: ya no es un solo archivo
json
// tsconfig.json
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}| Archivo | Configura | Se aplica a |
|---|---|---|
tsconfig.app.json | El código de tu aplicación (JSX, DOM, moduleResolution: "bundler") | src/ |
tsconfig.node.json | El entorno de Node que usa el propio Vite | vite.config.ts |
tsconfig.json | No compila nada — solo declara que existen los otros dos | — |
💡 Tu código de app corre en el navegador (necesita
lib: ["DOM"]) yvite.config.tscorre en Node al compilar (necesitatypes: ["node"]) — dos entornos distintos, dos archivos distintos.
🏗️ 8. npm run build: el comando que compila TODO
json
// package.json
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build", // compila TODOS los .ts/.tsx del proyecto antes de empaquetar
"preview": "vite preview"
}tsc -b (build mode) recorre todos los archivos .ts/.tsx incluidos por tsconfig.app.json y tsconfig.node.json, revisando tipos de principio a fin — no compila un archivo suelto como hacías con tsc archivo.ts en clases anteriores, sino el proyecto entero de una sola vez. Recién si tsc -b termina sin errores, sigue vite build y empaqueta todo para producción.
⚠️ El hallazgo más importante de esta clase, verificado paso a paso:
npm run devno revisa tipos en absoluto. Metí a propósitoconst numero: number = "esto no es un number"enApp.tsxcon el servidor de desarrollo corriendo — Vite hizo el HMR normal, sin ninguna queja, y la página siguió funcionando. Recién al corrernpm run buildaparecieron los errores reales:src/App.tsx(9,9): error TS2322: Type 'string' is not assignable to type 'number'. src/App.tsx(9,9): error TS6133: 'numero' is declared but its value is never read.
npm run devusa un transpilador rápido que solo quita los tipos, sin verificarlos — la verificación de tipos de verdad ocurre entsc -b, que solo corre connpm run build.
🐛 9. El bug real: React Compiler + Vite 8 (Rolldown) rompe el HMR
Esto me pasó de verdad al seguir el material original y elegir "la opción 2" (sección 3). Documento el diagnóstico completo porque es el tipo de problema real que vas a encontrar trabajando con herramientas que evolucionan rápido.
Síntoma: pantalla en negro al abrir npm run dev (o al guardar un cambio en App.tsx — el primer render a veces funcionaba, pero el HMR posterior rompía todo).
Causa raíz, confirmada revisando los paquetes instalados:
| Configuración con el bug | Configuración corregida | |
|---|---|---|
vite | ^8.2.0 (resolvía a 8.2.1) | ^7.3.6 |
| Dependencia interna de Vite | rolldown: ~1.2.0 (bundler experimental en Rust) | esbuild + rollup (el Vite clásico) |
| Plugin de React | @vitejs/plugin-react + @rolldown/plugin-babel + babel-plugin-react-compiler | Solo @vitejs/plugin-react |
vite.config.ts | react() + babel({ presets: [reactCompilerPreset()] }) | Solo react() |
Verifiqué con npm view vite@8.2.0 dependencies que esa versión sí trae rolldown como dependencia (es la build experimental "Rolldown-Vite"), mientras que npm view vite@7.3.6 dependencies confirma el Vite clásico (esbuild + rollup, sin rolldown). La combinación de ese Vite experimental con el plugin de Babel que React Compiler necesita para inyectar su lógica de memoización rompe el sistema de Fast Refresh — el runtime que reconecta un componente actualizado sin perder su estado — y lanza RefreshRuntime.register is not a function en cuanto React intenta reconectar el componente tras un HMR.
La solución, verificada en tu proyecto real:
json
// package.json — antes
"devDependencies": {
"@rolldown/plugin-babel": "^0.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"vite": "^8.2.0"
}json
// package.json — después
"devDependencies": {
"@vitejs/plugin-react": "^4.7.0",
"vite": "^7.3.6"
}typescript
// vite.config.ts — antes
plugins: [react(), babel({ presets: [reactCompilerPreset()] })]
// vite.config.ts — después
plugins: [react()]Con la configuración corregida, repetí exactamente la misma prueba del Ejercicio de HMR (editar el texto del botón con el servidor corriendo) y no hubo ningún error en consola, el contador conservó su valor, y no volvió a aparecer la pantalla en negro.
💡 La lección de fondo: cuando una herramienta te ofrece una opción marcada como "experimental" o muy nueva (Rolldown-Vite, React Compiler) junto a la clásica y estable, elegir la experimental por costumbre o por posición en una lista puede traer bugs reales que no tienen nada que ver con tu código — tenían que ver con la combinación de herramientas, no con ningún error tuyo.
🎨 9.1. Un segundo incidente real: App.css descuadrado + falso positivo del editor
Ya con la configuración de la sección 9 corregida (Vite 7 estable, sin React Compiler), apareció un segundo problema en la misma carpeta — sin ninguna relación con Vite ni con el HMR. Vale la pena documentarlo porque mezcla dos causas distintas que a primera vista parecen el mismo error, y aprender a distinguirlas es la parte útil.
Síntoma: la pestaña de App.css en VS Code mostraba "9+" (y luego "30") problemas, con el minimapa lleno de líneas rojas de arriba a abajo.
🅰️ Causa 1 (real): 5 líneas borradas por accidente
Editando el archivo a mano, se borraron sin querer estas líneas — el cierre de &:focus-visible, el cierre de .counter, una línea en blanco, y la apertura de .hero {:
css
/* App.css — roto */
&:focus-visible {
outline: 2px solid var(--accent);
position: relative; /* ← esto debía estar dentro de .hero, no de .counter */
.base,css
/* App.css — correcto */
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,Al faltar las dos llaves de cierre (} }), todo el resto del archivo (.base, .framework, .vite, #center…) quedó atrapado dentro de .counter en vez de ser bloques propios — por eso el error se repetía en cascada hasta el final del archivo, no solo en el punto donde faltaban las líneas.
Verificación sin abrir el editor: contar las llaves de apertura y cierre con awk — si no coinciden, el archivo está descuadrado seguro:
$ awk 'BEGIN{o=0;c=0} {o+=gsub(/{/,"{"); c+=gsub(/}/,"}")} END{print "open:",o," close:",c}' src/App.css
open: 32 close: 32💡 Restauré las 5 líneas a mano y volví a correr el mismo
awk— pasó de un conteo desigual a 32 y 32 exactos. Es la forma más rápida de confirmar "el archivo ya está bien formado" sin depender de lo que diga el editor.
🅱️ Causa 2 (falso positivo): el validador CSS de VS Code no entiende el anidamiento nativo
Con el CSS ya arreglado (llaves balanceadas), la pestaña seguía mostrando errores. Abrir el panel de problemas (Cmd+Shift+M) mostró la causa real:
at-rule or selector expected css(css-ruleorselectorexpected) [Lín. 13, col. 3]
} expected css(css-rcurlyexpected) [Lín. 23, col. 3]
{ expected css(css-lcurlyexpected) [Lín. 27, col. 11]Los 30 errores del panel tenían todos el mismo prefijo: css(...) — es decir, vienen del motor CSS integrado de VS Code (vscode-css-languageservice), no de un plugin de Stylelint (que se vería como stylelint(...)). Ese motor todavía no reconoce bien la sintaxis "relajada" del anidamiento nativo de CSS cuando el selector hijo va sin & delante:
css
.hero {
position: relative;
.base, /* ← el editor espera "& .base," o una @regla aquí, se confunde */
.framework,
.vite {
inset-inline: 0;
}
}Es sintaxis válida — equivalente a .hero .base { ... } — y tanto el navegador como Vite ya la procesaban sin problema (la página cargaba bien). El editor, no.
| Causa 1 (real) | Causa 2 (falso positivo) | |
|---|---|---|
| ¿El CSS estaba mal? | Sí — llaves descuadradas | No — sintaxis válida |
| ¿Vite/el navegador se quejaban? | No llegó a probarse (rompía el editor primero) | No, nunca — la página renderizaba bien |
| Código del error en Problems | css(css-rcurlyexpected) en el punto exacto del corte | css(css-ruleorselectorexpected) repetido en cada selector anidado sin & |
| Se arregla... | ...restaurando las líneas borradas | ...ajustando la configuración del editor |
La solución para la causa 2, sin tocar ni una línea de CSS — un .vscode/settings.json por proyecto:
json
// .vscode/settings.json
{
// El validador CSS integrado de VS Code aún no reconoce bien el anidamiento
// nativo (selectores hijos sin "&" delante, como en App.css). Marca falsos
// errores ("at-rule or selector expected") aunque la sintaxis es válida y
// el navegador/Vite la procesan bien. Se desactiva solo para este proyecto.
"css.validate": false
}Y recargar la ventana (Cmd+Shift+P → "Reload Window") para que el ajuste se aplique.
⚠️ La lección de fondo: un "9+" en la pestaña no siempre significa que tu código esté mal — puede ser tu código (causa 1, hay que arreglarlo) o puede ser una limitación del editor (causa 2, hay que ajustar la config). La forma de distinguirlos es objetiva: contar llaves con una herramienta ajena al editor (
awk), y leer el código exacto del error en el panel de problemas en vez de confiar solo en el conteo de la pestaña.
🧪 Tip de entrevista: ¿cómo confirmarías que un archivo de configuración/código está bien formado sin depender del editor? Con una herramienta de línea de comandos que no dependa de heurísticas de IDE — para llaves,
awk/grep -c; para JSON,jqonode -e "JSON.parse(...)"; para JS/TS, el propio compilador (tsc --noEmit). Un editor puede tener bugs o quedarse atrás con sintaxis nueva; el compilador o una cuenta exacta, no.
🆕 10. Resumen: lo que cambió desde que se grabó el material original
| En el material original | Verificado en este equipo (2026) |
|---|---|
| Elegir "TypeScript + SWC" como opción 2 de una lista | La opción 2 hoy es "TypeScript + React Compiler" — un nombre distinto, mismo número de lista |
| ~141 paquetes al instalar | 142 paquetes con la configuración corregida (casi idéntico) — o más si sumas los paquetes extra de React Compiler |
| Plantilla minimalista, "count is" en la línea 22 | Landing page completa, contador en la línea 29 |
build corre tsc | build corre tsc -b (build mode, por los references) |
| No se menciona React Compiler ni Rolldown-Vite | Existen y se ofrecen por defecto en el asistente — y su combinación causa un bug real de Fast Refresh (sección 9) |
📝 Sobre SWC específicamente: el paquete
@vitejs/plugin-react-swcsigue existiendo en npm — solo que ya no se ofrece con ese nombre en el asistente. Si quieres SWC en vez de Babel, hoy tendrías que instalarlo y configurarlo tú mismo envite.config.ts.
🧩 11. El proyecto progresivo: Nexus, un panel de empleados
A partir de aquí la clase cambia de forma. En vez de ejercicios sueltos, un solo proyecto — plantillaPlatzi — se construye ejercicio a ejercicio: cada uno agrega código real encima del anterior, nunca lo reemplaza desde cero. Es el mismo React + TypeScript ya visto en las secciones 1-10, pero conectado a una API real en vez de solo mostrar un contador.
La API que consume: el servidor Express + SQLite construido en la Clase 17 — la misma carpeta 02-Ejercicios/API/, corriendo en http://localhost:3000. Nada de esto usa datos inventados: cada fetch de esta clase golpea una base de datos SQLite real, con las mismas tablas (empleados, departamentos, usuarios) y la misma autenticación JWT que se construyó allá.
🗺️ Diagrama: cómo se relacionan los lenguajes del proyecto

💡 Fíjate en algo que se repite en los dos lados del diagrama: TypeScript nunca corre directamente, ni en el navegador ni en Node — siempre se compila primero a JavaScript (con Vite/esbuild en el cliente, con
tscen el servidor). Es el mismo lenguaje, pero cada lado lo compila con una herramienta distinta y para un propósito distinto: el cliente prioriza velocidad de recarga (Vite ni revisa tipos endev, como viste en la sección 8), el servidor prioriza seguridad de tipos antes de desplegar (tsc -b/tscsí los revisa siempre). El SQL, en cambio, nunca sale del servidor — el navegador jamás ve una consulta SQL, solo el JSON que Express arma a partir del resultado.
💡 Por eso conviene tener las dos clases abiertas en paralelo: la API de la Clase 17 corriendo con
npm run dev(onode dist/server.js) en una terminal, y este proyecto de React connpm run deven otra. Sin la API arriba, elfetchde esta clase falla conFailed to fetch— no es un bug de React, es que no hay servidor escuchando en el puerto 3000.
Cómo crece la carpeta src/ a lo largo de los ejercicios:
src/
├── types/
│ └── Empleado.ts ← Ejercicio 13: las formas de los datos
├── services/
│ ├── empleadosService.ts ← Ejercicio 17: todo el fetch, en un solo lugar
│ └── authService.ts ← Ejercicio 22: registro y login
├── components/
│ ├── EmpleadoCard.tsx ← Ejercicio 14
│ ├── FormularioEmpleado.tsx← Ejercicio 18
│ ├── FormularioAuth.tsx ← Ejercicio 22
│ ├── PanelBusqueda.tsx ← Ejercicio 26
│ └── PanelEstadisticas.tsx ← Ejercicio 28
└── App.tsx ← el que orquesta todo, crece en cada ejercicio🧪 Entrevista: ¿por qué separar
types/,services/ycomponents/en vez de escribir todo dentro deApp.tsx? Porque cada carpeta responde una pregunta distinta —types/responde "¿qué forma tienen mis datos?",services/responde "¿cómo hablo con el backend?",components/responde "¿cómo se ve cada pieza en pantalla?". Un componente nunca llama afetchdirectamente — llama a una función del servicio, que a su vez arma la URL y los headers. Si mañana cambia la URL de la API o el formato de autenticación, tocas un solo archivo, no cada componente que hace una petición.
💻 PARTE PRÁCTICA
Carpeta trabajada: 02-Ejercicios/React/plantillaPlatzi/ — creada con el asistente real, diagnosticada y corregida (sección 9), con npm install y npm run dev ya verificados arriba con la configuración estable.
bash
cd 02-Ejercicios/React
npm create vite@latest
# Project name: plantillaPlatzi
# Package name: plantillaplatzi
# Select a framework: React
# Select a variant: TypeScript ← NO "TypeScript + React Compiler"
cd plantillaPlatzi
npm install
npm run devVerificado en Chrome real, sirviendo tu proyecto ya con la configuración estable:
Get started
Edit src/App.tsx and save to test HMR
Contador 0Compilación de producción verificada también:
$ npm run build
✓ modules transformed.
✓ built.Todo verificado y corriendo, sin la pantalla en negro. ✅
🏋️ EJERCICIOS CON SOLUCIÓN
Ejercicio 1 — Crear un segundo proyecto con el asistente, eligiendo bien la variante
Crea otro proyecto llamado mi-segunda-app. En el paso "Select a variant", elige explícitamente TypeScript (no "TypeScript + React Compiler").
💡 ¿Sabías que…? — leer el nombre completo evita el bug de esta clase
La lección de la sección 3 de la teoría: el asistente cambia con el tiempo, pero los nombres de las opciones son más estables que su posición en la lista. Lee siempre el texto completo antes de confirmar.
Ver solución
bash
cd 02-Ejercicios/React
npm create vite@latest mi-segunda-app -- --template react-ts
cd mi-segunda-app
npm install
npm run dev💡
--template react-tsapunta directo a la variante "TypeScript" simple (sin Compiler), saltándose las preguntas interactivas — la forma más rápida de asegurarte de no repetir el error de esta clase.
Ejercicio 2 — Cambiar el texto y confirmar el HMR sin recargar
En App.tsx, cambia Contador {count} por cualquier otro texto, guarda, y confirma en el navegador que cambió sin que tú recargues la página.
💡 ¿Sabías que…? — HMR es distinto a "recarga automática"
Live Server (Clase 12) recarga la página completa cuando detecta un cambio — pierdes cualquier estado. HMR de Vite reemplaza solo el módulo que cambió, sin recargar la página entera — por eso el estado de React sobrevive.
Ver solución
typescript
<button ... >
Clics: {count}
</button>Terminal del servidor tras guardar:
[vite] (client) hmr update /src/App.tsxEjercicio 3 — Confirmar que el HMR preserva el estado
Haz clic en el botón contador 3 veces, y luego cambia cualquier otro texto en App.tsx (no el contador). Guarda. ¿El contador sigue en 3 o se reinició a 0?
💡 ¿Sabías que…? — React Fast Refresh es lo que preserva el estado
Es la misma tecnología que se rompió en la sección 9 de la teoría cuando se combinó mal con React Compiler + Rolldown-Vite. Cuando funciona bien (configuración estable), conserva el estado entre actualizaciones sin que hagas nada especial.
Ver solución
Verificado con la configuración corregida: el contador sigue en el mismo número después del HMR — no se reinicia a 0.
Ejercicio 4 — Forzar un error de tipos y ver que npm run dev lo ignora
Agrega const x: number = "texto"; en App.tsx con el servidor de desarrollo corriendo. ¿La página se rompe?
💡 ¿Sabías que…? — `npm run dev` transpila, no verifica tipos
El servidor de desarrollo solo quita las anotaciones de tipo para generar JavaScript, sin comprobarlas. La verificación real vive en tsc -b, que solo corre con npm run build.
Ver solución
Con npm run dev corriendo, guardas — la terminal solo muestra el HMR normal, sin ningún error, y la página sigue funcionando.
[vite] (client) hmr update /src/App.tsx ← nada de error, aunque el tipo esté malEjercicio 5 — Confirmar que npm run build SÍ detecta ese mismo error
Con el error de tipos del Ejercicio 4 todavía en App.tsx, corre npm run build.
💡 ¿Sabías que…? — el build usa `tsc -b`, el dev no usa `tsc` en absoluto
npm run build ejecuta tsc -b && vite build — verificación de tipos completa antes de empaquetar. npm run dev nunca invoca a tsc.
Ver solución
bash
npm run build$ npm run build
src/App.tsx(9,9): error TS2322: Type 'string' is not assignable to type 'number'.💡 Quita esa línea antes de seguir.
Ejercicio 6 — Leer la cadena de tsconfig
Abre tsconfig.app.json y busca la opción lib. ¿Qué incluye, y por qué tsconfig.node.json no la tiene?
💡 ¿Sabías que…? — `lib` declara qué APIs globales existen en tu entorno
lib le dice a TypeScript qué declaraciones de tipos globales debe cargar — sin "DOM", TypeScript ni reconoce document o window como existentes.
Ver solución
json
"lib": ["ES2023", "DOM"] // tsconfig.app.json — corre en el navegador
"lib": ["ES2023"] // tsconfig.node.json — corre en Node, sin DOMEjercicio 7 — Comprobar si tu versión de Vite depende de Rolldown
Corre npm view vite@TU_VERSION dependencies (con la versión real de tu package.json) y revisa si aparece rolldown en la lista.
💡 ¿Sabías que…? — puedes verificar esto ANTES de que te dé un bug
Esta es exactamente la comprobación que usé para diagnosticar la sección 9: comparar las dependencias de la versión de Vite que tienes instalada, para saber si estás en la rama clásica (esbuild/rollup) o en la experimental (rolldown).
Ver solución
bash
npm view vite@7.3.6 dependenciesTerminal real — sin rolldown en la lista (Vite clásico, estable):
esbuild: '^0.27.0 || ^0.28.0', rollup: '^4.43.0', ...Contra vite@8.2.0, que sí trae rolldown: '~1.2.0' en su lista — la pista que confirma cuál es la versión experimental.
Ejercicio 8 — Revisar qué genera npm run build
Corre npm run build (sin errores de tipos) y explora la carpeta dist/.
💡 ¿Sabías que…? — `dist/` es la versión "lista para subir a un servidor"
vite build junta todos tus archivos en unos pocos .js/.css optimizados y minificados — la carpeta dist/ es literalmente lo que subirías a un hosting, igual que la carpeta public/ de la Clase 13, pero generada automáticamente.
Ver solución
bash
npm run build
find dist -maxdepth 2dist/index.html
dist/assets/index-*.css
dist/assets/index-*.js💡 Los nombres con hash evitan que el navegador sirva una versión vieja en caché.
Ejercicio 9 — Reproducir el diagnóstico: comparar package.json con y sin React Compiler
Sin instalar nada, compara de memoria (o mirando la sección 9) qué tres paquetes aparecen SOLO en la versión con el bug, y qué línea de vite.config.ts cambia.
💡 ¿Sabías que…? — diagnosticar un bug real es comparar "antes" contra "después"
No hacía falta entender cada detalle interno de React Compiler o de Rolldown para resolver el bug — bastó con comparar qué cambió entre la versión rota y una versión que sabemos que funciona, y revertir esa diferencia puntual.
Ver solución
Los tres paquetes exclusivos de la versión con el bug: @rolldown/plugin-babel, babel-plugin-react-compiler, y vite en su rango ^8.2.0 (en vez de ^7.3.6). La línea de vite.config.ts que cambia es la lista de plugins: de [react(), babel({ presets: [reactCompilerPreset()] })] a solo [react()].
Ejercicio 10 — Integrador: recrear el proyecto desde cero, sin repetir el error
Borra (o renombra) plantillaPlatzi y créala de nuevo desde el asistente interactivo, prestando atención esta vez al nombre completo de cada opción del paso "Select a variant".
💡 ¿Sabías que…? — este ejercicio es literalmente lo que hice yo para corregir mi proyecto
No hay ningún concepto nuevo — es aplicar la lección completa de la clase: leer el asistente con cuidado, elegir "TypeScript" (no "TypeScript + React Compiler"), y verificar con npm view vite@version dependencies que no dependa de rolldown antes de darlo por bueno.
Ver solución
bash
npm create vite@latest plantillaPlatzi -- --template react-ts
cd plantillaPlatzi
npm install
npm run devVerificado: con react-ts (sin Compiler) el package.json resultante nunca incluye @rolldown/plugin-babel ni babel-plugin-react-compiler — evitas el bug de raíz, sin necesidad de diagnosticarlo después.
Ejercicio 11 — Detectar un CSS descuadrado sin abrir el editor
Usa un solo comando de terminal para confirmar si src/index.css tiene la misma cantidad de llaves { de apertura que de } de cierre.
💡 ¿Sabías que…? — gsub de awk cuenta ocurrencias de un patrón
gsub(patrón, reemplazo) en awk reemplaza todas las ocurrencias de un patrón en la línea actual y devuelve cuántas reemplazó — un truco común para contar caracteres sin instalar nada extra. Ejemplo de referencia, contando comillas dobles en un .json en vez de llaves en un .css:
bash
awk 'BEGIN{n=0} {n+=gsub(/"/,"\"")} END{print "comillas:", n}' package.jsonVer solución
bash
awk 'BEGIN{o=0;c=0} {o+=gsub(/{/,"{"); c+=gsub(/}/,"}")} END{print "open:",o," close:",c}' src/index.cssopen: 15 close: 15💡 Si los dos números no coinciden, el archivo está descuadrado seguro — sin necesidad de leerlo entero ni confiar en lo que marque el editor.
Ejercicio 12 — Diferenciar un error real de un falso positivo del editor
Un archivo .css muestra "9+" problemas en su pestaña de VS Code. ¿Qué dos pasos harías, en orden, para saber si son errores reales de sintaxis o falsos positivos del validador integrado?
💡 ¿Sabías que…? — el código del error dice de dónde viene
En el panel de problemas (Cmd+Shift+M), cada error trae entre paréntesis su fuente: css(...) es del motor integrado de VS Code, stylelint(...) sería de la extensión Stylelint si estuviera instalada. Saber la fuente te dice si el arreglo es en tu código o en la configuración del editor — ver Clase 12 sobre distinguir errores de "tu código" contra errores de "la herramienta".
Ver solución
- Contar llaves con
awk(Ejercicio 11). Si abren y cierran igual, el CSS no tiene un error de sintaxis real. - Abrir el panel de problemas (
Cmd+Shift+M) y leer el código exacto entre paréntesis. Si todos soncss(css-ruleorselectorexpected)/css(css-lcurlyexpected)/css(css-rcurlyexpected)apuntando a selectores anidados sin&, es el validador integrado sin soporte completo de anidamiento nativo — un falso positivo. Se arregla con"css.validate": falseen.vscode/settings.json(sección 9.1), no editando el CSS.
Ejercicio 13 — Modelar los datos: la interfaz Empleado
Antes de pedir nada a la API, define la forma de un empleado. Crea src/types/Empleado.ts con una interfaz Empleado (id, nombre, cargo, salario) y una lista estática de tres empleados escrita a mano, sin fetch todavía.
💡 ¿Sabías que…? — tipar antes de conectar la API evita bugs de forma
Definir la interfaz primero, con datos inventados, te deja verificar que el resto del componente (el .map(), las props) compila bien antes de meter la complejidad extra de una petición de red. Si algo falla después de conectar el fetch, sabes que el problema está en la red o en el tipo de la respuesta, no en cómo pintas los datos.
Ver solución
typescript
// src/types/Empleado.ts
export interface Empleado {
id: number;
nombre: string;
cargo: string;
salario: number;
}tsx
// App.tsx — datos estáticos, todavía sin fetch
const empleados: Empleado[] = [
{ id: 1, nombre: 'Ana Torres', cargo: 'Desarrolladora', salario: 3200 },
{ id: 2, nombre: 'Luis Peña', cargo: 'Diseñador', salario: 2800 },
{ id: 3, nombre: 'Carla Ríos', cargo: 'Gerente de Proyecto', salario: 4100 },
];Ejercicio 14 — Pintar la lista con un componente propio
Saca la tarjeta de cada empleado a su propio componente, EmpleadoCard.tsx, que recibe un Empleado por props y lo muestra en un <li>.
💡 ¿Sabías que…? — la key del .map() debe ser estable, no el índice
Usar el id real como key (en vez del índice del array) importa en cuanto la lista cambia de orden o se eliminan elementos — con el índice, React puede confundir qué tarjeta es cuál y reusar el estado del componente equivocado. Con el id, cada tarjeta queda ligada a su empleado sin importar en qué posición esté.
Ver solución
tsx
// src/components/EmpleadoCard.tsx
import type { Empleado } from '../types/Empleado';
interface Props {
empleado: Empleado;
}
export function EmpleadoCard({ empleado }: Props) {
return (
<li>
<strong>{empleado.nombre}</strong> — {empleado.cargo} (${empleado.salario})
</li>
);
}tsx
// App.tsx
<ul>
{empleados.map((emp) => (
<EmpleadoCard key={emp.id} empleado={emp} />
))}
</ul>Ejercicio 15 — Conectar con la API real: fetch + useEffect + estado de carga
Reemplaza el array estático por un fetch real a GET http://localhost:3000/empleados (la ruta que construiste en la Clase 17), guardado en useState, disparado una sola vez con useEffect. Agrega un estado cargando y muestra "Cargando empleados…" mientras la petición está en vuelo.
💡 ¿Sabías que…? — el array vacío [] de useEffect significa "una sola vez"
useEffect(fn, []) ejecuta fn después del primer render y nunca más (a menos que el componente se desmonte y remonte). Sin el array, fn correría después de cada render — con un fetch adentro, eso sería una petición infinita en bucle.
Ver solución
tsx
// App.tsx
const [empleados, setEmpleados] = useState<Empleado[]>([]);
const [cargando, setCargando] = useState(true);
useEffect(() => {
fetch('http://localhost:3000/empleados')
.then((res) => res.json())
.then(setEmpleados)
.finally(() => setCargando(false));
}, []);
if (cargando) return <p>Cargando empleados…</p>;Verificado en Chrome real: con la API de la Clase 17 corriendo, la lista muestra los tres empleados sembrados en db.ts (Ana Torres, Luis Peña, Carla Ríos) — datos reales de SQLite, no inventados en el componente.
Ejercicio 16 — Manejar el error cuando la API no responde
Detén el servidor de la Clase 17 y recarga la app de React. En vez de quedarse en "Cargando…" para siempre, agrega un estado error que se muestre en pantalla.
💡 ¿Sabías que…? — fetch NO rechaza la promesa en un 404/500
fetch solo lanza un error de red (.catch) si la petición ni siquiera pudo salir (servidor caído, sin internet, CORS bloqueado). Un 404 o 500 sigue siendo una respuesta "exitosa" desde el punto de vista de fetch — por eso hay que revisar res.ok explícitamente para tratarlo como error.
Ver solución
tsx
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('http://localhost:3000/empleados')
.then((res) => {
if (!res.ok) throw new Error('No se pudo cargar la lista de empleados');
return res.json();
})
.then(setEmpleados)
.catch((err: Error) => setError(err.message))
.finally(() => setCargando(false));
}, []);Verificado: con el servidor de la Clase 17 apagado, el fetch rechaza con TypeError: Failed to fetch, el .catch lo captura, y la pantalla muestra ⚠️ No se pudo cargar la lista de empleados en vez de quedarse cargando para siempre.
Ejercicio 17 — Extraer el fetch a una capa de servicio
Saca la lógica de la petición de App.tsx a un archivo nuevo, src/services/empleadosService.ts, con una función listarEmpleados(). App.tsx deja de saber la URL de la API — solo llama a la función.
💡 ¿Sabías que…? — esta es la misma idea de services/ que ya viste en el backend
En la Clase 17, empleados.service.ts separó la lógica de SQL de las rutas de Express. Aquí es el mismo patrón, del lado del cliente: empleadosService.ts separa la lógica de fetch de los componentes que la usan. Ningún componente sabe que la URL es http://localhost:3000 — si mañana cambia, se edita en un solo lugar.
Ver solución
typescript
// src/services/empleadosService.ts
import type { Empleado } from '../types/Empleado';
const API_URL = 'http://localhost:3000';
export async function listarEmpleados(): Promise<Empleado[]> {
const res = await fetch(`${API_URL}/empleados`);
if (!res.ok) throw new Error('No se pudo cargar la lista de empleados');
return res.json();
}tsx
// App.tsx
import { listarEmpleados } from './services/empleadosService';
useEffect(() => {
listarEmpleados()
.then(setEmpleados)
.catch((err: Error) => setError(err.message))
.finally(() => setCargando(false));
}, []);Ejercicio 18 — Crear un empleado: formulario controlado + POST
Crea FormularioEmpleado.tsx con tres <input> controlados (nombre, cargo, salario) y una función crearEmpleado en el servicio que hace POST /empleados. Al enviar, refresca la lista con listarEmpleados() de nuevo.
💡 ¿Sabías que…? — "controlado" significa que React es la única fuente de verdad
En un formulario controlado, el value de cada <input> viene del estado de React (useState), y cada tecla dispara onChange para actualizar ese estado — el DOM nunca "guarda" el valor por su cuenta. Es lo opuesto a un formulario no controlado (useRef + leer .value al enviar), que viste como alternativa más simple pero con menos control sobre lo que el usuario escribe en cada momento.
Ver solución
typescript
// src/types/Empleado.ts — se agrega la forma que espera el POST (sin id)
export interface EmpleadoNuevo {
nombre: string;
cargo: string;
salario: number;
}typescript
// empleadosService.ts
export async function crearEmpleado(datos: EmpleadoNuevo): Promise<void> {
const res = await fetch(`${API_URL}/empleados`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos),
});
if (!res.ok) throw new Error('No se pudo crear el empleado');
}tsx
// FormularioEmpleado.tsx
const [datos, setDatos] = useState<EmpleadoNuevo>({ nombre: '', cargo: '', salario: 0 });
function manejarEnvio(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
onGuardar(datos);
}
<form onSubmit={manejarEnvio}>
<input value={datos.nombre} onChange={(e) => setDatos({ ...datos, nombre: e.target.value })} />
{/* cargo y salario iguales, salario con Number(e.target.value) */}
<button type="submit">Crear empleado</button>
</form>Verificado en el navegador: crear "Empleado Clase 15 — QA Automation ($3300)" desde el formulario lo persiste de verdad en nexus.db — recargar la página lo sigue mostrando.
Ejercicio 19 — Reutilizar el mismo formulario para editar
En vez de crear un segundo formulario, reutiliza FormularioEmpleado agregándole una prop empleadoAEditar: Empleado | null. Cuando no es null, precarga sus datos y el onGuardar llama a actualizarEmpleado (PUT) en vez de crearEmpleado.
💡 ¿Sabías que…? — useEffect puede reaccionar a un cambio de props, no solo montar una vez
useEffect(() => setDatos(...), [empleadoAEditar]) corre cada vez que la prop empleadoAEditar cambia — así el formulario se "recarga" con los datos del empleado que acabas de elegir editar, sin desmontar el componente.
Ver solución
typescript
// empleadosService.ts
export async function actualizarEmpleado(id: number, datos: EmpleadoNuevo): Promise<void> {
const res = await fetch(`${API_URL}/empleados/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos),
});
if (!res.ok) throw new Error('No se pudo actualizar el empleado');
}tsx
// FormularioEmpleado.tsx
useEffect(() => {
setDatos(empleadoAEditar ? { ...empleadoAEditar } : VACIO);
}, [empleadoAEditar]);tsx
// App.tsx
async function manejarGuardar(datos: EmpleadoNuevo) {
if (empleadoAEditar) {
await actualizarEmpleado(empleadoAEditar.id, datos);
} else {
await crearEmpleado(datos);
}
setEmpleadoAEditar(null);
cargarEmpleados();
}Verificado: hacer clic en "Editar" sobre Ana Torres precarga el formulario con sus datos actuales; cambiar el cargo y guardar actualiza esa fila sin crear una nueva.
Ejercicio 20 — Eliminar un empleado y refrescar la lista
Agrega un botón "Eliminar" a EmpleadoCard que llame a eliminarEmpleado(id) (DELETE) y vuelva a cargar la lista.
💡 ¿Sabías que…? — un DELETE exitoso normalmente no trae cuerpo
La ruta DELETE /empleados/:id de la Clase 17 responde 204 No Content — "éxito, y no hay nada que devolverte". Por eso eliminarEmpleado solo revisa res.ok, sin intentar leer res.json() (leer JSON de una respuesta vacía lanzaría un error de parseo).
Ver solución
typescript
// empleadosService.ts
export async function eliminarEmpleado(id: number): Promise<void> {
const res = await fetch(`${API_URL}/empleados/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('No se pudo eliminar el empleado');
}tsx
// EmpleadoCard.tsx
<button onClick={() => onEliminar(empleado.id)}>Eliminar</button>Ejercicio 21 — Tipar la respuesta con el JOIN a departamentos
La Clase 17 evolucionó GET /empleados para incluir, vía LEFT JOIN, el nombre del departamento de cada empleado (departamento: string | null, null si no tiene asignado). Actualiza la interfaz Empleado de React para reflejar esa forma real, y muéstrala en EmpleadoCard.
💡 ¿Sabías que…? — el tipo del cliente debe seguir al tipo real del servidor, no al revés
Si Empleado en React no incluyera departamento, TypeScript no se quejaría — simplemente ignorarías un campo que la API sí te está mandando. Los tipos de un cliente HTTP no se verifican automáticamente contra el servidor; hay que actualizarlos a mano cada vez que el contrato cambia (por eso son tan útiles herramientas como Swagger — la Clase 17 documentó su API en /docs — para no perder de vista el contrato real).
Ver solución
typescript
// src/types/Empleado.ts
export interface Empleado {
id: number;
nombre: string;
cargo: string;
salario: number;
departamento: string | null; // viene del LEFT JOIN, del lado del servidor
}tsx
// EmpleadoCard.tsx
<span>
<strong>{empleado.nombre}</strong> — {empleado.cargo} (${empleado.salario})
{empleado.departamento && ` · ${empleado.departamento}`}
</span>Ejercicio 22 — Registrar un usuario nuevo desde React
La Clase 17 agregó POST /usuarios (registro) y POST /login. Crea src/services/authService.ts con una función registrar(email, password), y un formulario simple (FormularioAuth.tsx) que la use.
💡 ¿Sabías que…? — la contraseña viaja en texto plano por HTTPS, pero nunca se guarda así
El fetch manda password como texto normal en el body — eso es seguro porque viaja cifrado por HTTPS (o por localhost en desarrollo). Lo que nunca debe pasar es que el servidor la guarde tal cual: la Clase 17 la hashea con bcryptjs antes de tocar la base de datos (usuarios.service.ts). React no tiene ni necesita saber nada de ese hash — solo manda el texto plano una vez, por la conexión segura.
Ver solución
typescript
// src/services/authService.ts
const API_URL = 'http://localhost:3000';
export async function registrar(email: string, password: string): Promise<void> {
const res = await fetch(`${API_URL}/usuarios`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const cuerpo = await res.json().catch(() => ({}));
throw new Error(cuerpo.error ?? 'No se pudo registrar el usuario');
}
}Verificado contra la API real: registrar un email ya usado responde 409 con {"error":"Ese email ya está registrado"} — el mismo mensaje que definió la ruta en la Clase 17, mostrado tal cual en la interfaz.
Ejercicio 23 — Iniciar sesión y guardar el JWT
Agrega login(email, password): Promise<string> a authService.ts, que llama a POST /login y devuelve el token. Guárdalo en el estado de App.tsx y en localStorage, para que sobreviva un refresh de página.
💡 ¿Sabías que…? — useState con una función de inicialización solo corre una vez
useState(() => localStorage.getItem('token')) — pasar una función (en vez del valor directo) hace que React solo la ejecute en el primer render, no en cada re-render. Si escribieras useState(localStorage.getItem('token')) a secas, localStorage.getItem se llamaría de nuevo en cada render — funciona igual, pero desperdicia trabajo innecesario.
Ver solución
typescript
// authService.ts
export async function login(email: string, password: string): Promise<string> {
const res = await fetch(`${API_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const cuerpo = await res.json().catch(() => ({}));
throw new Error(cuerpo.error ?? 'No se pudo iniciar sesión');
}
const { token } = await res.json();
return token;
}tsx
// App.tsx
const [token, setToken] = useState<string | null>(() => localStorage.getItem('token'));
async function manejarIniciarSesion(email: string, password: string) {
const nuevoToken = await login(email, password);
localStorage.setItem('token', nuevoToken);
setToken(nuevoToken);
}Verificado end-to-end contra la API real: registrar prueba@nexus.com, luego loguear con esas mismas credenciales, devuelve un JWT válido (200, {"token":"eyJhbGc..."}).
Ejercicio 24 — Adjuntar el token en las peticiones de escritura
Desde la Clase 17, POST/PUT/PATCH/DELETE de /empleados exigen un header Authorization: Bearer <token> (middleware verificarToken). Actualiza crearEmpleado, actualizarEmpleado y eliminarEmpleado para recibir el token y mandarlo, y oculta el formulario y los botones de escritura cuando no hay sesión.
💡 ¿Sabías que…? — el mismo fetch, con un header más, es toda la diferencia
No hace falta ninguna librería especial para "hacer login" desde React — un JWT es solo una cadena de texto que se manda en un header normal de HTTP. Toda la "autenticación" del lado del cliente se reduce a: guardar esa cadena en algún lado (aquí, localStorage) y pegarla en el header correcto de cada petición protegida.
Ver solución
typescript
// empleadosService.ts
function encabezadosAuth(token: string | null): HeadersInit {
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
export async function crearEmpleado(datos: EmpleadoNuevo, token: string | null): Promise<void> {
const res = await fetch(`${API_URL}/empleados`, {
method: 'POST',
headers: encabezadosAuth(token),
body: JSON.stringify(datos),
});
if (!res.ok) {
const cuerpo = await res.json().catch(() => ({}));
throw new Error(cuerpo.error ?? 'No se pudo crear el empleado');
}
}
// actualizarEmpleado y eliminarEmpleado siguen el mismo patróntsx
// App.tsx
{token ? (
<FormularioEmpleado empleadoAEditar={empleadoAEditar} onGuardar={manejarGuardar} onCancelar={...} />
) : (
<FormularioAuth onIniciarSesion={manejarIniciarSesion} onRegistrar={registrar} />
)}
<EmpleadoCard empleado={emp} soloLectura={!token} onEditar={...} onEliminar={...} />Verificado con curl, comparando los dos casos: DELETE /empleados/5 sin header Authorization responde 401 {"error":"Falta el token..."}; el mismo DELETE con Authorization: Bearer <token> válido responde 204. La UI reproduce exactamente esa misma diferencia — sin sesión, no hay ni siquiera un botón "Eliminar" que probar.
Ejercicio 25 — Manejar un token inválido o expirado
Los JWT de la Clase 17 expiran en 1 hora ({ expiresIn: '1h' }). Simula esa situación manualmente (borra un carácter del token guardado en localStorage con las DevTools) e intenta crear un empleado. ¿Qué mensaje ve el usuario?
💡 ¿Sabías que…? — un 401 no distingue "sin token" de "token roto"
El middleware verificarToken de la Clase 17 responde el mismo 401 tanto si falta el header como si jwt.verify() lanza una excepción (firma inválida, token expirado, token editado a mano) — solo cambia el texto del error. Del lado de React, ambos casos se manejan igual: el catch del fetch muestra el mensaje que vino del servidor, sin necesidad de lógica especial para cada motivo.
Ver solución
Con el token corrupto en localStorage, crearEmpleado recibe 401 {"error":"Token inválido o expirado"} de la API, y el catch en App.tsx lo muestra tal cual: ⚠️ Token inválido o expirado. La solución del lado del usuario es simple: cerrar sesión (localStorage.removeItem('token')) y volver a loguearse.
💡 Un proyecto real llevaría esto un paso más allá: interceptar cualquier
401de cualquier petición y forzar el cierre de sesión automáticamente, en vez de esperar a que el usuario lea el mensaje de error. Con solofetch(sin una librería de peticiones), eso significaría revisarres.status === 401en cada función del servicio y llamar a una funcióncerrarSesion()compartida.
Ejercicio 26 — Buscar por nombre con un query param real
Agrega un <input> de búsqueda que mande ?nombre=... a GET /empleados (la Clase 17 ya soporta ese filtro con LIKE). El filtrado ocurre en SQL, no con .filter() en el array ya cargado en el navegador.
💡 ¿Sabías que…? — filtrar en el servidor escala; filtrar en el navegador no
Con .filter() en React tendrías que descargar todos los empleados primero, y recién ahí filtrar en memoria — bien con 5 registros, inviable con 500,000. Mandar el filtro como query param deja que SQLite haga el trabajo con un índice, y el navegador solo recibe los resultados que realmente necesita mostrar.
Ver solución
typescript
// empleadosService.ts
export interface FiltrosEmpleados {
nombre?: string;
}
export async function listarEmpleados(filtros: FiltrosEmpleados = {}): Promise<Empleado[]> {
const params = new URLSearchParams();
if (filtros.nombre) params.set('nombre', filtros.nombre);
const res = await fetch(`${API_URL}/empleados?${params.toString()}`);
if (!res.ok) throw new Error('No se pudo cargar la lista de empleados');
return res.json();
}tsx
// App.tsx
const [filtros, setFiltros] = useState<FiltrosEmpleados>({});
useEffect(() => { listarEmpleados(filtros).then(setEmpleados)... }, [filtros]);
<input onChange={(e) => setFiltros({ ...filtros, nombre: e.target.value || undefined })} />Verificado en el navegador: escribir "ana" en el buscador deja solo "Ana Torres — Desarrolladora ($3200) · Tecnología" en la lista — el resto de los empleados desaparece sin que React haya tocado un array en memoria, todo vino filtrado desde GET /empleados?nombre=ana.
Ejercicio 27 — Ordenar y paginar desde la URL de la API
Extiende FiltrosEmpleados con orden ('nombre' | 'salario' | 'cargo' | 'id') y pagina, agregando un <select> y dos botones de paginación.
💡 ¿Sabías que…? — el orden tiene una lista blanca del lado del servidor
La Clase 17 valida orden contra COLUMNAS_ORDEN_VALIDAS antes de meterlo en el SQL — si mandaras orden=algoInventado, el servidor lo ignora silenciosamente y usa id por defecto, en vez de dejar que ese texto llegue crudo a una consulta SQL (eso sería una inyección SQL). React no necesita replicar esa validación — pero si el <select> solo ofrece esas cuatro opciones, nunca vas a mandar algo distinto.
Ver solución
typescript
// empleadosService.ts
export interface FiltrosEmpleados {
nombre?: string;
orden?: 'nombre' | 'salario' | 'cargo' | 'id';
limite?: number;
pagina?: number;
}tsx
// PanelBusqueda.tsx
<select value={filtros.orden ?? 'id'} onChange={(e) => onCambiar({ ...filtros, orden: e.target.value as FiltrosEmpleados['orden'] })}>
<option value="id">Sin orden</option>
<option value="nombre">Nombre</option>
<option value="salario">Salario</option>
<option value="cargo">Cargo</option>
</select>
<button disabled={(filtros.pagina ?? 1) <= 1} onClick={() => onCambiar({ ...filtros, pagina: (filtros.pagina ?? 1) - 1 })}>
← Anterior
</button>
<span>Página {filtros.pagina ?? 1}</span>
<button onClick={() => onCambiar({ ...filtros, pagina: (filtros.pagina ?? 1) + 1 })}>
Siguiente →
</button>💡 El servidor usa
limite = 10por defecto (LIMIT ? OFFSET ?enempleados.service.ts) — no hace falta que React lo mande explícitamente para que la paginación funcione.
Ejercicio 28 — Panel de estadísticas y el indicador de caché
Consume GET /empleados/estadisticas (total, promedio, máximo) en un componente PanelEstadisticas.tsx, y muéstralo arriba de la lista. La Clase 17 agregó una caché en memoria a esa ruta — la respuesta trae un campo desdeCache: boolean.
💡 ¿Sabías que…? — una caché sin invalidar es peor que no tener caché
El truco de cache.ts en la Clase 17 no es guardar el valor — es borrarlo (invalidarCache) cada vez que crearEmpleado, actualizarEmpleado o eliminarEmpleado cambian algo que afecta las estadísticas. Sin esa invalidación, el panel de React mostraría un promedio viejo después de crear un empleado nuevo, aunque la lista de abajo sí se hubiera actualizado — una inconsistencia silenciosa y difícil de detectar en pruebas manuales rápidas.
Ver solución
typescript
// src/types/Empleado.ts
export interface EstadisticasEmpleados {
total: number;
promedio: number;
maximo: number;
desdeCache: boolean;
}typescript
// empleadosService.ts
export async function obtenerEstadisticas(): Promise<EstadisticasEmpleados> {
const res = await fetch(`${API_URL}/empleados/estadisticas`);
if (!res.ok) throw new Error('No se pudieron cargar las estadísticas');
return res.json();
}tsx
// PanelEstadisticas.tsx
export function PanelEstadisticas({ estadisticas }: Props) {
if (!estadisticas) return null;
return (
<div className="panel-estadisticas">
<span>Total: {estadisticas.total}</span>
<span>Promedio: ${estadisticas.promedio.toFixed(2)}</span>
<span>Máximo: ${estadisticas.maximo}</span>
{estadisticas.desdeCache && <span>⚡ desde caché</span>}
</div>
);
}Verificado en el navegador, con los tres empleados sembrados: Total: 3 · Promedio: $3366.67 · Máximo: $4100. Al crear un empleado nuevo y volver a consultar, el promedio se recalcula (la caché se invalidó del lado del servidor); dos consultas seguidas sin cambios de por medio, en cambio, la segunda sí trae ⚡ desde caché.
Ejercicio 29 — Integrador: el flujo completo, de principio a fin
Con la API de la Clase 17 corriendo, verifica en el navegador — sin recargar entre pasos — la secuencia completa: (1) ver la lista sin sesión, sin botones de escritura; (2) registrar una cuenta; (3) iniciar sesión; (4) crear un empleado; (5) editarlo; (6) buscarlo por nombre; (7) eliminarlo; (8) cerrar sesión y confirmar que los botones de escritura vuelven a desaparecer.
💡 ¿Sabías que…? — este es exactamente el recorrido que verifiqué yo mismo para escribir esta clase
No hay ningún paso nuevo — es encadenar, en una sola sesión de navegador, cada pieza que construiste en los ejercicios 13 al 28. Si algún paso falla, casi siempre la causa es una de dos: la API de la Clase 17 no está corriendo, o el token guardado en localStorage quedó de una sesión vieja contra una base de datos que ya cambió.
Ver solución
Los ocho pasos, verificados de punta a punta contra la API real:
- Sin sesión — la lista carga (
GET /empleadosno exige token), pero ninguna tarjeta muestra "Editar" ni "Eliminar" — solo aparece el formulario de login. - Registro —
POST /usuarioscon un email nuevo responde201. - Login —
POST /logincon esas credenciales responde200y un JWT; aparece el botón "Cerrar sesión" y el formulario de crear empleado. - Crear — el formulario hace
POST /empleadoscon el headerAuthorization; el nuevo empleado aparece al final de la lista. - Editar — clic en "Editar" precarga el formulario; guardar hace
PUT /empleados/:idy la tarjeta se actualiza en el mismo lugar de la lista. - Buscar — escribir parte del nombre en el buscador deja solo esa tarjeta.
- Eliminar — clic en "Eliminar" hace
DELETE /empleados/:id; la tarjeta desaparece de la lista. - Cerrar sesión —
localStorage.removeItem('token'); los botones de escritura desaparecen de nuevo, aunque la lista se siga viendo (modo solo lectura).
Cero errores en la consola del navegador en todo el recorrido.
❓ Preguntas y respuestas (autoevaluación)
1. ¿Qué es Vite, en una frase?
Una herramienta que genera plantillas de proyectos frontend ya construidas, para no tener que configurar compilador, paquetes y estructura a mano.
2. ¿Por qué los componentes de React se escriben en archivos .tsx y no .ts?
Porque
.tsxhabilita JSX — la sintaxis tipo HTML que React usa dentro de JavaScript/TypeScript para describir la interfaz.
3. En el paso "Select a variant" del asistente, ¿qué diferencia hay entre "TypeScript" y "TypeScript + React Compiler"?
"TypeScript" (
react-ts) es la plantilla simple, estable, con@vitejs/plugin-reactnormal. "TypeScript + React Compiler" (react-compiler-ts) agrega un plugin de Babel que memoiza automáticamente tus componentes — más nuevo y, en la combinación verificada en esta clase, con un bug real de Fast Refresh.
4. ¿Qué error apareció exactamente cuando el HMR se rompió?
RefreshRuntime.register is not a function, causado por la combinación de Vite 8 (Rolldown-Vite, experimental) con el plugin de Babel de React Compiler.
5. ¿Cómo confirmo si mi versión de Vite es la experimental (Rolldown) o la clásica?
Con
npm view vite@TU_VERSION dependencies— si aparecerolldownen la lista, es la variante experimental. Si aparecenesbuildyrollupsinrolldown, es la clásica y estable.
6. ¿npm run dev revisa errores de tipos en tu código?
No. Solo transpila (quita los tipos). La revisión de tipos real ocurre con
tsc -b, que solo corre dentro denpm run build.
7. ¿Por qué el script build usa tsc -b en vez de tsc a secas?
Porque el proyecto usa
referencesentre variostsconfig.json(appynode) —-bes el "build mode" diseñado para compilar proyectos organizados así, revisando tipos en todo el proyecto de una vez.
8. ¿Qué es HMR y en qué se diferencia de una recarga completa de página?
Hot Module Replacement: reemplaza solo el módulo que cambió, sin recargar la página entera — por eso el estado de React (como un contador) sobrevive.
9. Si un material de estudio dice "elige la opción número dos" en un asistente de línea de comandos, ¿por qué puede ser un consejo poco confiable con el tiempo?
Porque las herramientas evolucionan y la lista de opciones cambia — la posición "número dos" de hoy puede ser una opción totalmente distinta a la de cuando se escribió el material. Es más confiable guiarse por el nombre de la opción que por su posición en la lista.
10. ¿Cuál fue la solución real para arreglar el bug de esta clase?
Bajar
vitede^8.2.0a^7.3.6(la rama estable, sin Rolldown) y quitar el plugin de React Compiler (@rolldown/plugin-babel+babel-plugin-react-compiler) depackage.jsonyvite.config.ts, dejando solo@vitejs/plugin-reactnormal.
11. Tenías 30 errores css(css-ruleorselectorexpected) en App.css. ¿Cómo confirmaste que eran falsos positivos y no un error tuyo de verdad?
Primero conté las llaves
{y}del archivo conawk— dieron exactamente iguales (32 y 32), así que el CSS estaba bien formado. Después confirmé en el panel de problemas que el código de cada error empezaba concss(...)— el motor CSS integrado de VS Code, no Stylelint — y que apuntaban a selectores anidados sin&delante, una sintaxis válida que ese motor todavía no reconoce del todo.
12. En el proyecto Nexus, ¿por qué App.tsx nunca llama a fetch directamente, sino a funciones como listarEmpleados()?
Porque la lógica de red vive en una capa de servicio (
empleadosService.ts,authService.ts) separada de los componentes — el mismo patrónservices/que ya usaste del lado del backend en la Clase 17. Si la URL de la API o el formato de autenticación cambian, se edita en un solo archivo, no en cada componente.
13. ¿Por qué useEffect(fn, []) con un array vacío solo corre una vez, y qué pasaría si se te olvida el array?
El array de dependencias le dice a React cuándo volver a ejecutar el efecto — vacío significa "nunca, salvo el primer render". Sin el array,
fnse ejecutaría después de cada render; con unfetchadentro, eso sería una petición en bucle infinito.
14. ¿Por qué fetch no lanza un error automáticamente ante un 404 o un 500?
Porque, desde el punto de vista de
fetch, la petición sí llegó y sí volvió con una respuesta — eso ya es "éxito de red". Por eso hay que revisarres.oka mano y lanzar el error tú mismo;fetchsolo rechaza la promesa si la petición ni siquiera pudo salir (servidor caído, sin conexión, CORS bloqueado).
15. ¿Qué diferencia hay entre un formulario controlado y uno no controlado en React?
En uno controlado, el
valuede cada input viene deuseStatey cada tecla actualiza ese estado cononChange— React es la única fuente de verdad. En uno no controlado, el DOM guarda el valor por su cuenta y tú lo lees recién al enviar (típicamente conuseRef).
16. ¿Cómo protege la API de la Clase 17 las rutas de escritura de /empleados, y qué pasa si React no manda el header correspondiente?
Con el middleware
verificarToken, que exigeAuthorization: Bearer <token>. Sin ese header, la API responde401("Falta el token...") antes de tocar la base de datos — React nunca llega a ejecutar elPOST/PUT/DELETEcon éxito, aunque el resto del código esté bien.
17. ¿Dónde se guarda el JWT en el proyecto Nexus, y por qué ahí y no solo en una variable de estado de React?
En
localStorage, además del estado de React. Una variable de estado se pierde en cuanto el usuario recarga la página;localStoragepersiste entre recargas, así que la sesión sobrevive un F5 sin obligar a loguearse de nuevo.
18. En la búsqueda de empleados por nombre, ¿el filtrado ocurre en React o en el servidor? ¿Por qué importa la diferencia?
Ocurre en el servidor, vía
GET /empleados?nombre=...conLIKEen SQL. Filtrar en el servidor evita tener que descargar todos los registros antes de mostrar solo algunos — con una tabla grande, filtrar en el navegador con.filter()sería insostenible.
19. ¿Qué representa el campo desdeCache en la respuesta de /empleados/estadisticas, y qué lo invalida?
Indica si el servidor sirvió el resultado desde una caché en memoria (
cache.ts, unMap) en vez de recalcularlo con una consulta de agregación. Se invalida cada vez quecrearEmpleado,actualizarEmpleadooeliminarEmpleadocambian algo que afecta el total, el promedio o el máximo.
📎 Apuntes relacionados
- Clase 2:
!(aserción no-nula) vs?. - Clase 12:
<script>, servir un proyecto en el navegador - Clase 13:
outDir,tsc -p, organizarsrc/public - Clase 14:
querySelector, type assertions,tsc --watch - Conceptos
- Comandos
➡️ Siguiente
Utility types — Partial, Pick, Omit, Record… transformar tipos existentes sin reescribirlos.