Tienes el proyecto de CicloUrbano creado y funcionando, pero todo lo que ves en pantalla es la plantilla de ejemplo de Vite. Es hora de borrarla y escribir el primer código verdaderamente tuyo. En esta lección desmontarás pieza a pieza el mecanismo que hace que React aparezca en la página, entenderás la relación exacta entre index.html y src/main.jsx, y crearás tus dos primeros componentes: Bienvenida y TarjetaBicicleta. Al terminar sabrás qué es un componente, cómo se escribe, cómo se usa y por qué su nombre debe empezar obligatoriamente por mayúscula.

Contenido

  1. Limpiar la plantilla de ejemplo
  2. Anatomía de index.html: el punto de anclaje
  3. src/main.jsx línea a línea
  4. Tu primer componente: Bienvenida
  5. Un componente es una función que devuelve JSX
  6. La convención de la mayúscula inicial
  7. Segundo componente: TarjetaBicicleta
  8. React.StrictMode: qué es y por qué ves cosas dos veces
  9. Ver el cambio en caliente

  1. Limpiar la plantilla de ejemplo

Vite genera una demostración con un contador y logotipos. Es útil para comprobar que todo funciona, pero estorba para empezar. Con el servidor de desarrollo corriendo (npm run dev), haz esta limpieza.

Borra estos ficheros, que no vamos a necesitar:

rm src/App.css
rm src/assets/react.svg
rm public/vite.svg

Vacía src/index.css y déjalo con unos estilos base sobrios para CicloUrbano:

/* src/index.css — estilos globales de CicloUrbano */
:root {
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  line-height: 1.6;
  color: #1f2933;
  background-color: #f5f7fa;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 2rem;
}

h1,
h2,
h3 {
  line-height: 1.25;
  margin-top: 0;
}

Sustituye por completo el contenido de src/App.jsx:

// src/App.jsx
function App() {
  return (
    <main>
      <h1>CicloUrbano</h1>
    </main>
  );
}

export default App;

Guarda y mira el navegador: la demostración ha desaparecido y queda un título. Si ves un error en pantalla o en la consola, casi seguro es porque App.jsx sigue importando ./App.css o ./assets/react.svg, ficheros que acabas de borrar. Revisa que no quede ningún import sobrante.

Este App.jsx mínimo ya contiene todos los conceptos de la lección: una función llamada App, que devuelve algo que parece HTML, y que se exporta. Vamos a entender cada parte.

  1. Anatomía de index.html: el punto de anclaje

Recuerda el HTML del proyecto, ya con el título de CicloUrbano:

<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>CicloUrbano — Alquiler de bicicletas urbanas</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

Esto es lo que ocurre cuando alguien abre la página:

  1. El navegador descarga este HTML. El <body> contiene un único div vacío. En ese momento la página está literalmente en blanco.
  2. Encuentra la etiqueta <script type="module"> y descarga /src/main.jsx (en desarrollo lo sirve Vite ya transformado; en producción será el paquete generado por npm run build).
  3. Ese script ejecuta React, que crea los nodos del DOM y los inserta dentro de div#root.
  4. La página aparece.

De ahí viene el nombre de SPA (Single Page Application): existe una sola página HTML real y todo el contenido lo genera JavaScript dentro de un contenedor. El id="root" no es mágico: podría llamarse app o ciclourbano siempre que main.jsx lo busque con el mismo nombre. root es la convención universal, así que consérvala.

sequenceDiagram
    participant N as Navegador
    participant H as index.html
    participant M as main.jsx
    participant R as React
    N->>H: Solicita la página
    H-->>N: HTML con div#root vacío
    N->>M: Carga el módulo /src/main.jsx
    M->>R: createRoot(div#root)
    R->>R: Ejecuta el componente App
    R-->>N: Inserta los nodos dentro de div#root

  1. src/main.jsx línea a línea

Este es el fichero completo:

// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.jsx';

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

Cinco líneas de importación y una sentencia. Vamos una por una.

import { StrictMode } from 'react'; Del paquete react importamos StrictMode, un componente especial de ayuda al desarrollo. Lo veremos en el apartado 8.

import { createRoot } from 'react-dom/client'; Aquí está la separación de responsabilidades que mencionamos en la lección 01-01: react sabe describir interfaces; react-dom sabe pintarlas en un navegador. El subcamino /client indica que es el renderizador de cliente (existe también react-dom/server para renderizado en servidor, tema del Módulo 10).

createRoot crea una raíz de React: la conexión entre un nodo del DOM real y el árbol de componentes que React va a gestionar dentro de él.

Importante: createRoot es la API moderna, introducida en React 18. En tutoriales antiguos verás ReactDOM.render(<App />, document.getElementById('root')). Esa API está eliminada en React 19 y no funcionará. Usa siempre createRoot.

import './index.css'; Importar un CSS desde JavaScript resulta chocante la primera vez, pero es una funcionalidad de la herramienta de construcción, no de React: Vite ve el import, procesa la hoja de estilos y la inyecta en la página. Como no extraemos ningún valor del fichero, no hay llaves ni nombre de variable.

import App from './App.jsx'; Importación por defecto: por eso no lleva llaves. Se corresponde con el export default App; del final de App.jsx. Al ser por defecto, el nombre local es libre —podrías escribir import Raiz from './App.jsx'—, aunque por claridad se usa siempre el mismo.

La sentencia final. Léela de dentro hacia fuera:

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);
  1. document.getElementById('root') es JavaScript del DOM de toda la vida: obtiene el div vacío del HTML.
  2. createRoot(...) convierte ese div en una raíz gestionada por React y devuelve un objeto con un método render.
  3. .render(...) recibe la descripción de la interfaz y la pinta dentro de la raíz.
  4. <App /> es tu componente usado como si fuera una etiqueta. Fíjate en la barra final: los componentes sin contenido interno se autocierran, igual que <img /> o <input />.

Es la única vez en toda la aplicación que llamarás a createRoot. A partir de aquí, todo lo demás es componentes dentro de componentes.

  1. Tu primer componente: Bienvenida

Crea la carpeta donde vivirán los componentes de CicloUrbano y, dentro, el primero:

mkdir -p src/componentes
// src/componentes/Bienvenida.jsx
function Bienvenida() {
  return (
    <header>
      <h1>CicloUrbano</h1>
      <p>Alquila una bicicleta urbana en tu barrio, por horas y sin complicaciones.</p>
    </header>
  );
}

export default Bienvenida;

Analicemos las cuatro partes:

  • function Bienvenida(): es una función de JavaScript normal y corriente. Sin parámetros, de momento.
  • return (...): devuelve la descripción de un trozo de interfaz escrita en JSX. Los paréntesis tras return no son obligatorios cuando el JSX cabe en una línea, pero sí lo son en la práctica cuando ocupa varias: sin ellos, el punto y coma automático de JavaScript insertaría un ; justo después de return y la función devolvería undefined.
  • Un único elemento raíz: todo el contenido está envuelto en <header>. React necesita devolver una sola cosa; profundizarás en esta regla y en los fragmentos en la lección JSX.
  • export default Bienvenida;: hace el componente importable desde otros ficheros.

Ahora úsalo en App.jsx:

// src/App.jsx
import Bienvenida from './componentes/Bienvenida.jsx';

function App() {
  return (
    <main>
      <Bienvenida />
    </main>
  );
}

export default App;

Guarda y mira el navegador: aparecen el título y el eslogan de CicloUrbano. Acabas de escribir y usar tu primer componente de React.

  1. Un componente es una función que devuelve JSX

Esta frase merece que la interiorices, porque es la definición completa:

Un componente de React es una función de JavaScript cuyo nombre empieza por mayúscula y que devuelve una descripción de interfaz (JSX).

No hay herencia, ni clases, ni registro en ningún sitio, ni configuración. Una función. Y como es una función, se usa como una etiqueta:

Lo que escribes Lo que hace React
<Bienvenida /> Llama a la función Bienvenida() y coloca en ese punto lo que devuelve
<Bienvenida></Bienvenida> Exactamente lo mismo; la forma autocerrada es la habitual cuando no hay contenido dentro
Bienvenida() Llama a la función directamente. No lo hagas: React deja de tratarlo como un componente y pierde su identidad para el estado, DevTools y la reconciliación

Puedes escribir el mismo componente con función flecha; ambas formas son equivalentes y verás las dos en proyectos reales:

// Declaración de función (la que usaremos en el curso)
function Bienvenida() {
  return <h1>CicloUrbano</h1>;
}

// Función flecha asignada a una constante
const Bienvenida = () => {
  return <h1>CicloUrbano</h1>;
};

// Función flecha con retorno implícito (sin llaves ni return)
const Bienvenida = () => <h1>CicloUrbano</h1>;

En este curso usaremos la declaración de función porque su nombre aparece siempre de forma clara en los mensajes de error y en React DevTools, y porque no depende de la posición en el fichero.

Y como los componentes son funciones que devuelven descripciones de interfaz, se componen entre sí formando un árbol:

flowchart TD
    R["Raíz de React (div#root)"] --> S[StrictMode]
    S --> A[App]
    A --> B[Bienvenida]
    A --> T[TarjetaBicicleta]

  1. La convención de la mayúscula inicial

No es una preferencia de estilo: es una regla obligatoria. JSX decide qué hacer con una etiqueta mirando la primera letra de su nombre.

<header>       // minúscula -> React entiende: etiqueta HTML estándar 'header'
<Bienvenida /> // mayúscula -> React entiende: mi componente llamado Bienvenida

La razón es que JSX se transforma en llamadas a una función, y en esa transformación la primera letra determina si el primer argumento es una cadena de texto o una referencia a tu función:

// Escribes esto:
<header />
<Bienvenida />

// Y se transforma, a grandes rasgos, en esto:
jsx('header', ...)      // cadena: React crea un <header> del navegador
jsx(Bienvenida, ...)    // referencia: React ejecuta tu función

Si escribes tu componente en minúscula, React buscará una etiqueta HTML llamada bienvenida, no la encontrará como elemento conocido y el navegador la mostrará vacía. No verás un error rojo, solo una página incompleta, lo que convierte este fallo en uno de los más frustrantes para quien empieza.

Nombre ¿Válido? Resultado
Bienvenida Componente correcto
TarjetaBicicleta Correcto; PascalCase para nombres compuestos
bienvenida No React busca una etiqueta HTML inexistente; no se pinta nada
tarjeta_bicicleta No Mismo problema, además de romper la convención de nombres

Regla práctica: componentes en PascalCase, ficheros con el mismo nombre que el componente. Bienvenida vive en Bienvenida.jsx, TarjetaBicicleta en TarjetaBicicleta.jsx. Así encontrar el código de lo que ves en pantalla es inmediato.

  1. Segundo componente: TarjetaBicicleta

Vamos a crear la pieza más característica de CicloUrbano: la tarjeta que representa una bicicleta. En esta lección los datos van escritos a mano dentro del componente; hacerla flexible con datos que llegan de fuera es el objetivo de la lección Props.

// src/componentes/TarjetaBicicleta.jsx
function TarjetaBicicleta() {
  return (
    <article className="tarjeta-bicicleta">
      <h3>Urbana Clásica</h3>
      <p>Tipo: urbana</p>
      <p>Estado: disponible</p>
      <p>Estación: Plaza Mayor</p>
      <p>
        <strong>2,50 € / hora</strong>
      </p>
    </article>
  );
}

export default TarjetaBicicleta;

Dos detalles que quizá te hayan llamado la atención:

  • className en lugar de class. class es una palabra reservada de JavaScript, y JSX es JavaScript. Por eso React usa className. Es el error de novato más frecuente y lo veremos a fondo en la próxima lección.
  • <article> y <h3> en lugar de div para todo. React no te libera de escribir HTML semántico: los componentes acaban produciendo etiquetas reales, y elegirlas bien beneficia a la accesibilidad y al SEO.

Los datos corresponden a la bicicleta bici-001 del dominio de CicloUrbano que fijamos en la lección 01-01.

Ahora úsalo, y de paso comprueba algo importante: el mismo componente se puede usar tantas veces como quieras.

// src/App.jsx
import Bienvenida from './componentes/Bienvenida.jsx';
import TarjetaBicicleta from './componentes/TarjetaBicicleta.jsx';

function App() {
  return (
    <main>
      <Bienvenida />

      <section>
        <h2>Bicicletas destacadas</h2>
        <TarjetaBicicleta />
        <TarjetaBicicleta />
        <TarjetaBicicleta />
      </section>
    </main>
  );
}

export default App;

En el navegador verás tres tarjetas idénticas. Que sean idénticas es precisamente la limitación que resolveremos con props: cada instancia recibirá su propia bicicleta y mostrará datos distintos. Pero el mecanismo de reutilización ya está a la vista, y es la esencia del modelo de componentes.

Añade unos estilos a src/index.css para que se vea presentable:

/* Añadir al final de src/index.css */
.tarjeta-bicicleta {
  background: #ffffff;
  border: 1px solid #d9e2ec;
  border-radius: 8px;
  padding: 1rem 1.25rem;
  margin-bottom: 1rem;
  max-width: 22rem;
}

.tarjeta-bicicleta h3 {
  margin-bottom: 0.5rem;
  color: #12805c;
}

.tarjeta-bicicleta p {
  margin: 0.25rem 0;
  font-size: 0.95rem;
}

  1. React.StrictMode: qué es y por qué ves cosas dos veces

Vuelve a main.jsx y fíjate en que <App /> está envuelto en <StrictMode>:

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

StrictMode es un componente especial que no pinta absolutamente nada: no genera ninguna etiqueta en el DOM. Su único cometido es activar comprobaciones adicionales durante el desarrollo para detectar código problemático cuanto antes.

Lo que hace:

  • Ejecuta tus componentes dos veces en cada render (solo en desarrollo). Sirve para descubrir efectos secundarios accidentales: si tu componente modifica una variable externa o hace algo más que calcular su interfaz, con dos ejecuciones el resultado será distinto y el fallo saldrá a la luz de inmediato en lugar de dentro de seis meses.
  • Monta, desmonta y vuelve a montar cada componente al inicio, para verificar que la limpieza de efectos está bien escrita.
  • Avisa de APIs obsoletas y de patrones desaconsejados.

De ahí la consecuencia que más desconcierta a quien empieza: si pones un console.log('renderizando') dentro de un componente, verás el mensaje dos veces en la consola. No es un fallo tuyo ni de React.

Compruébalo:

// src/componentes/Bienvenida.jsx
function Bienvenida() {
  console.log('Se está ejecutando Bienvenida');
  return (
    <header>
      <h1>CicloUrbano</h1>
      <p>Alquila una bicicleta urbana en tu barrio, por horas y sin complicaciones.</p>
    </header>
  );
}

export default Bienvenida;

La consola del navegador muestra el mensaje dos veces. Recuerda:

Con StrictMode (desarrollo) En producción
Ejecuciones del componente Dobles, a propósito Una sola
Efectos al montar Se ejecutan, limpian y repiten Una sola vez
Impacto en el usuario final Ninguno: solo afecta a npm run dev Ninguno

No lo quites. La tentación de eliminar StrictMode para que la consola quede limpia es grande y es un error: estarías desactivando el detector de fallos precisamente donde sirve. La duplicación de efectos y su relación con useEffect se retoma con detalle en la lección Hook useEffect.

Borra ya el console.log antes de continuar.

  1. Ver el cambio en caliente

Con todo montado, aprovecha para experimentar y afianzar lo aprendido. Con npm run dev corriendo y el navegador al lado del editor:

  1. Cambia el eslogan de Bienvenida y guarda. El texto se actualiza al instante, sin recargar.
  2. Cambia el modelo de TarjetaBicicleta a Eléctrica Pro y el precio a 4,00 € / hora. Las tres tarjetas cambian a la vez, porque las tres son la misma función devolviendo lo mismo.
  3. Abre React DevTools, pestaña Components. Verás el árbol App → Bienvenida y tres TarjetaBicicleta. Observa que StrictMode aparece en el árbol de React pero no en la pestaña Elements del navegador: confirma que no genera HTML.
  4. Comenta una de las tres <TarjetaBicicleta /> con {/* ... */} y comprueba que desaparece.

Ese bucle —editar, guardar, ver el resultado— es el ritmo de trabajo normal en React, y es la razón por la que el HMR de Vite importa tanto.

Errores Comunes y Consejos

  • Olvidar el export default. El import devuelve undefined y React lanza Element type is invalid: expected a string ... but got: undefined. Siempre que veas ese mensaje, revisa la pareja export/import del componente citado.
  • Confundir export por defecto y export con nombre. export default Bienvenida se importa como import Bienvenida from ...; export function Bienvenida se importa como import { Bienvenida } from .... Mezclarlos da undefined.
  • Nombrar el componente en minúscula. <bienvenida /> no da error visible: simplemente no pinta nada. Si un componente «no aparece» y la consola está limpia, comprueba la mayúscula inicial antes que nada.
  • Poner el return en una línea y el JSX en la siguiente sin paréntesis. La inserción automática de punto y coma hace que la función devuelva undefined y React se queje de que nada se ha devuelto. Abre paréntesis en la misma línea del return.
  • Usar ReactDOM.render. Está eliminado en React 19. Si un tutorial lo usa, está desactualizado; traduce a createRoot(...).render(...).
  • Llamar al componente como función, {Bienvenida()}. Funciona visualmente, pero React pierde la identidad del componente: no aparece en DevTools, no puede tener su propio estado ni participar bien en la reconciliación. Usa siempre <Bienvenida />.
  • Eliminar StrictMode porque «la consola sale doble». Es el comportamiento esperado en desarrollo y desactivarlo esconde fallos reales.
  • Consejo: un componente por fichero, con el mismo nombre que el fichero, en src/componentes/. Cuando CicloUrbano tenga treinta componentes lo agradecerás.

Ejercicios

Ejercicio 1

Crea un componente PieDePagina en src/componentes/PieDePagina.jsx que muestre, dentro de una etiqueta <footer>, el texto «CicloUrbano — Servicio de alquiler de bicicletas urbanas» y, en una segunda línea, «Estaciones: Plaza Mayor · Parque Norte · Estación Central». Úsalo en App.jsx debajo de las tarjetas.

Ejercicio 2

Este código no funciona. Localiza los tres errores y corrígelos explicando por qué falla cada uno.

// src/componentes/estacion.jsx
function estacion() {
  return
    <div class="estacion">
      <h3>Plaza Mayor</h3>
      <p>Barrio Centro · 20 plazas</p>
    </div>;
}

export estacion;

Ejercicio 3

Crea un componente TarjetaEstacion que muestre los datos de la estación est-02 del dominio de CicloUrbano (Parque Norte, barrio Norte, 15 plazas) con datos escritos a mano, y móntalo en App.jsx dentro de una <section> con el encabezado «Estaciones». Después responde: si quisieras mostrar las tres estaciones del dominio con este mismo componente, ¿qué limitación te encontrarías y en qué lección del curso se resuelve?

Soluciones

Solución 1.

// src/componentes/PieDePagina.jsx
function PieDePagina() {
  return (
    <footer>
      <p>CicloUrbano — Servicio de alquiler de bicicletas urbanas</p>
      <p>Estaciones: Plaza Mayor · Parque Norte · Estación Central</p>
    </footer>
  );
}

export default PieDePagina;
// src/App.jsx
import Bienvenida from './componentes/Bienvenida.jsx';
import TarjetaBicicleta from './componentes/TarjetaBicicleta.jsx';
import PieDePagina from './componentes/PieDePagina.jsx';

function App() {
  return (
    <main>
      <Bienvenida />

      <section>
        <h2>Bicicletas destacadas</h2>
        <TarjetaBicicleta />
        <TarjetaBicicleta />
        <TarjetaBicicleta />
      </section>

      <PieDePagina />
    </main>
  );
}

export default App;

Solución 2. Los tres errores:

  1. Nombre en minúscula. function estacion() hace que <estacion /> se interprete como una etiqueta HTML desconocida y no se pinte nada. Debe ser TarjetaEstacion (PascalCase), y por coherencia el fichero debe llamarse igual.
  2. return seguido de salto de línea sin paréntesis. JavaScript inserta un punto y coma automáticamente tras return, la función devuelve undefined y React protesta. Hay que abrir el paréntesis en la misma línea del return.
  3. class en vez de className. class es palabra reservada de JavaScript. En JSX se escribe className. (Y export estacion; tampoco es sintaxis válida: falta default.)
// src/componentes/TarjetaEstacion.jsx
function TarjetaEstacion() {
  return (
    <div className="estacion">
      <h3>Plaza Mayor</h3>
      <p>Barrio Centro · 20 plazas</p>
    </div>
  );
}

export default TarjetaEstacion;

Solución 3.

// src/componentes/TarjetaEstacion.jsx
function TarjetaEstacion() {
  return (
    <article className="tarjeta-estacion">
      <h3>Parque Norte</h3>
      <p>Barrio: Norte</p>
      <p>Plazas: 15</p>
    </article>
  );
}

export default TarjetaEstacion;
// src/App.jsx (fragmento)
<section>
  <h2>Estaciones</h2>
  <TarjetaEstacion />
</section>

La limitación: los datos están escritos dentro del componente, así que las tres instancias mostrarían exactamente «Parque Norte, Norte, 15 plazas». Para que cada tarjeta muestre una estación distinta, el componente tiene que recibir los datos desde fuera. Ese mecanismo son las props, y se estudia en la lección Props: Pasando Datos a Componentes. Pintar una lista completa a partir de un array se verá en Listas y Claves.

Conclusión

Has pasado de ejecutar la plantilla de Vite a escribir tus propios componentes. Ahora sabes que index.html aporta un div#root vacío, que main.jsx lo convierte en raíz de React con createRoot(...).render(...) —nunca con el obsoleto ReactDOM.render—, y que un componente no es más que una función con nombre en mayúscula que devuelve JSX y se usa como una etiqueta. Has creado Bienvenida y TarjetaBicicleta, has comprobado que un mismo componente se reutiliza cuantas veces haga falta y entiendes por qué StrictMode ejecuta tu código dos veces en desarrollo.

Todo lo que has escrito dentro del return es JSX, y hasta ahora lo has usado por intuición: parece HTML y funciona casi como HTML. Casi. En la siguiente lección, JSX: Extensión de Sintaxis de JavaScript, verás en qué se transforma realmente, qué reglas lo separan del HTML y cómo insertar valores dinámicos con llaves para que tus tarjetas dejen de ser texto fijo.

Curso de React

Módulo 1: Introducción a React

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados