React: fundamentos para aplicaciones cliente
Contenido
- Introducción
- Conocimiento previo
- Referencias
- Índice
- 1. Qué problema resuelve React
- 2. Preparar el entorno con Vite
- 3. Estructura inicial de un proyecto React
- 4. Primer componente React
- 5. JSX: escribir interfaz dentro de JavaScript
- 6. Composición de componentes
- 7. Props: pasar datos a un componente
- 8. Renderizado condicional
- 9. Listas y propiedad key
- 10. Eventos en React
- 11. Estado con useState
- 12. Actualizar estado a partir del estado anterior
- 13. Formularios controlados
- 14. Levantar el estado al componente padre
- 15. Estilos básicos en componentes
- 16. Conectar React con datos remotos
- 17. TypeScript como evolución natural
- 18. Proyecto guiado: gestor de productos
- 18.1. Crear datos iniciales
- 18.2. Crear el componente ProductoItem
- 18.3. Crear el componente ListaProductos
- 18.4. Crear el componente FormularioProducto
- 18.5. Conectar todo en App
- 19. Buenas prácticas iniciales
- 20. Ejercicios
Unidad didáctica de React para Desarrollo web en entorno cliente: entorno con Vite, JSX, componentes, props, estado, eventos, condicionales, listas, formularios y proyecto guiado.
Introducción
React es una biblioteca de JavaScript para construir interfaces de usuario mediante componentes. En lugar de modificar manualmente el DOM cada vez que cambia un dato, React permite describir cómo debe verse la interfaz para un estado determinado. Cuando el estado cambia, React vuelve a calcular la interfaz y actualiza lo necesario.
En esta unidad aprenderás los fundamentos de React desde cero. Partiremos de lo que ya conoces: JavaScript moderno, arrays, objetos, funciones, eventos, formularios y renderizado de datos. La diferencia será la forma de organizar la interfaz: ya no empezaremos seleccionando elementos con querySelector(), sino dividiendo la pantalla en componentes y pasando datos entre ellos.
El objetivo no es memorizar todas las APIs de React, sino entender su modelo mental: componentes, JSX, props, estado, eventos y renderizado declarativo. Con esa base podrás crear interfaces pequeñas y prepararte para la unidad avanzada.
Conocimiento previo
- HTML
- JavaScript
- JavaScript en el navegador: DOM, eventos, formularios y comunicación asíncrona
- Librerías y frameworks de cliente
- Uso básico de terminal, Node.js y npm.
- Comprensión de arrays, objetos, funciones flecha,
map(), eventos y formularios.
Referencias
- React - Learn
- React - Describing the UI
- React - Adding Interactivity
- React - Passing Props to a Component
- React - State: A Component’s Memory
- React - Rendering Lists
- React - Installation
- Vite - Getting Started
- TypeScript Handbook
Índice
- Qué problema resuelve React
- Preparar el entorno con Vite
- Estructura inicial de un proyecto React
- Primer componente React
- JSX: escribir interfaz dentro de JavaScript
- Composición de componentes
- Props: pasar datos a un componente
- Renderizado condicional
- Listas y propiedad key
- Eventos en React
- Estado con useState
- Actualizar estado a partir del estado anterior
- Formularios controlados
- Levantar el estado al componente padre
- Estilos básicos en componentes
- Conectar React con datos remotos
- TypeScript como evolución natural
- Proyecto guiado: gestor de productos 18.1. Crear datos iniciales 18.2. Crear el componente ProductoItem 18.3. Crear el componente ListaProductos 18.4. Crear el componente FormularioProducto 18.5. Conectar todo en App
- Buenas prácticas iniciales
- Ejercicios
1. Qué problema resuelve React
Con JavaScript puro puedes crear interfaces interactivas seleccionando elementos, escuchando eventos y modificando el DOM. Ese enfoque es directo y útil para páginas pequeñas, pero puede complicarse cuando muchos elementos dependen de los mismos datos.
Imagina una lista de productos con buscador, filtro, contador, formulario de alta y favoritos. Si cambian los productos, quizá tengas que actualizar manualmente:
- La lista visible.
- El número de resultados.
- El mensaje de estado.
- Los botones de favorito.
- El formulario.
- El contenido filtrado.
React propone organizar la interfaz como una función de los datos. En lugar de pensar “qué nodo debo cambiar ahora”, piensas “qué debe mostrar la interfaz con estos datos”.
Comparación inicial:
| DOM manual | React |
|---|---|
| Seleccionas elementos con selectores. | Defines componentes. |
| Cambias nodos paso a paso. | Describes la interfaz esperada. |
| Actualizas cada zona manualmente. | Cambias estado y React renderiza. |
| Reutilizar una parte exige crear funciones DOM. | Reutilizas componentes. |
| El código puede dispersarse por eventos y renderizados. | La lógica se agrupa por componente. |
React no elimina la necesidad de saber JavaScript. Al contrario: usa JavaScript intensivamente. Arrays, objetos, funciones, módulos, desestructuración y eventos aparecen constantemente.
2. Preparar el entorno con Vite
Para practicar React usaremos un proyecto creado con Vite. Vite proporciona un servidor de desarrollo rápido, recarga automática y construcción de producción.
Comprueba antes que tienes Node.js y npm instalados:
node --versionnpm --versionCrear un proyecto React con Vite:
npm create vite@latest react-productos -- --template reactDespués entra en la carpeta e instala dependencias:
cd react-productosnpm installArranca el servidor de desarrollo:
npm run devEl terminal mostrará una URL local, normalmente parecida a http://localhost:5173/. Al abrirla en el navegador verás la aplicación React inicial.
3. Estructura inicial de un proyecto React
Un proyecto creado con Vite suele tener una estructura parecida a esta:
react-productos/├─ index.html├─ package.json├─ src/│ ├─ App.jsx│ ├─ main.jsx│ ├─ App.css│ └─ index.css└─ vite.config.jsArchivos importantes:
| Archivo | Función |
|---|---|
index.html | Documento HTML base donde se monta la aplicación. |
src/main.jsx | Punto de entrada JavaScript que conecta React con el DOM. |
src/App.jsx | Componente principal inicial de la aplicación. |
package.json | Dependencias y scripts del proyecto. |
vite.config.js | Configuración de Vite. |
En main.jsx verás una idea parecida a esta:
import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';import App from './App.jsx';import './index.css';
createRoot(document.getElementById('root')).render( <StrictMode> <App /> </StrictMode>,);React se monta sobre un elemento del HTML, normalmente un div con id="root". A partir de ahí, React gestiona la interfaz que cuelga de ese punto.
4. Primer componente React
Un componente React es una función de JavaScript que devuelve JSX. Por convención, los componentes empiezan con mayúscula.
function Saludo() { return <h1>Hola, React</h1>;}
export default Saludo;Puedes usar ese componente dentro de otro:
function App() { return ( <main> <Saludo /> </main> );}
export default App;Puntos importantes:
Saludoes una función, pero se usa como etiqueta:<Saludo />.- El nombre empieza por mayúscula para diferenciarlo de etiquetas HTML.
- El componente devuelve una descripción de interfaz.
export defaultpermite importar el componente desde otro archivo.
Un componente debe tener un propósito claro. Si crece demasiado, probablemente conviene dividirlo en componentes más pequeños.
5. JSX: escribir interfaz dentro de JavaScript
JSX permite describir estructura de interfaz dentro de JavaScript. Se parece a HTML, pero no es exactamente HTML.
Ejemplo:
function Producto() { const nombre = 'Teclado'; const precio = 29.99;
return ( <article className="producto"> <h2>{nombre}</h2> <p>{precio} €</p> </article> );}Reglas básicas de JSX:
| Regla | Ejemplo |
|---|---|
Usar className en lugar de class. | <section className="panel"> |
| Cerrar todas las etiquetas. | <img src="logo.png" alt="Logo" /> |
| Devolver una sola raíz o un fragmento. | <>...</> |
| Insertar expresiones JavaScript con llaves. | {nombre} |
Usar htmlFor en etiquetas label. | <label htmlFor="email"> |
JSX permite mezclar estructura y expresiones, pero no debe convertirse en un bloque difícil de leer. Si un componente acumula demasiada lógica dentro del return, conviene extraer variables o componentes.
6. Composición de componentes
Componer componentes significa construir una interfaz grande a partir de piezas pequeñas.
Ejemplo:
function Cabecera() { return <h1>Catálogo de productos</h1>;}
function Pie() { return <p>Aplicación de ejemplo para aprender React.</p>;}
function App() { return ( <main> <Cabecera /> <p>Contenido principal</p> <Pie /> </main> );}La composición permite:
- Reutilizar partes de la interfaz.
- Separar responsabilidades.
- Leer la estructura general con más facilidad.
- Probar cambios en una parte sin revisar toda la aplicación.
Una buena pregunta al crear componentes es: “¿esta parte tiene sentido como bloque independiente?”. Si la respuesta es sí, puede ser un componente.
7. Props: pasar datos a un componente
Las props son datos que un componente recibe desde su componente padre.
Ejemplo básico:
function TarjetaProducto({ nombre, precio }) { return ( <article className="producto"> <h2>{nombre}</h2> <p>{precio} €</p> </article> );}
function App() { return <TarjetaProducto nombre="Teclado" precio={29.99} />;}Fíjate en dos detalles:
- Los textos pueden pasarse entre comillas.
- Los números y expresiones JavaScript se pasan entre llaves.
También puedes pasar un objeto completo:
function TarjetaProducto({ producto }) { return ( <article className="producto"> <h2>{producto.nombre}</h2> <p>{producto.precio} €</p> </article> );}
const producto = { id: 1, nombre: 'Ratón', precio: 14.99,};
function App() { return <TarjetaProducto producto={producto} />;}Las props deben tratarse como datos de entrada. Un componente no debería modificar directamente las props que recibe.
8. Renderizado condicional
El renderizado condicional permite mostrar una parte de la interfaz solo si se cumple una condición.
Ejemplo con if:
function MensajeEstado({ cargando, error }) { if (cargando) { return <p>Cargando datos...</p>; }
if (error) { return <p>No se han podido cargar los datos.</p>; }
return <p>Datos listos.</p>;}Ejemplo con operador ternario:
function ProductoDisponible({ disponible }) { return ( <p>{disponible ? 'Disponible' : 'Sin stock'}</p> );}Ejemplo con &&:
function Aviso({ mostrar }) { return ( <section> {mostrar && <p>Revisa los datos antes de guardar.</p>} </section> );}Usa el estilo que resulte más claro. Si hay varias condiciones complejas, suele ser mejor usar if antes del return.
9. Listas y propiedad key
Para renderizar listas en React se usa normalmente map().
const productos = [ { id: 1, nombre: 'Teclado' }, { id: 2, nombre: 'Ratón' }, { id: 3, nombre: 'Monitor' },];
function ListaProductos() { return ( <ul> {productos.map((producto) => ( <li key={producto.id}>{producto.nombre}</li> ))} </ul> );}La propiedad key ayuda a React a identificar cada elemento cuando la lista cambia. Debe ser estable y única dentro de esa lista.
Buenas opciones para key:
- Un
idreal de la base de datos. - Un identificador creado al añadir el elemento.
- Un código único del recurso.
Evita usar el índice del array como key si la lista puede reordenarse, filtrarse o eliminar elementos. Puede provocar comportamientos confusos cuando React reutiliza elementos.
10. Eventos en React
En React los eventos se escriben como props dentro del JSX. Los nombres usan camelCase: onClick, onChange, onSubmit.
function BotonContador() { function manejarClick() { console.log('Clic en el botón'); }
return <button onClick={manejarClick}>Sumar</button>;}No llames la función directamente dentro del JSX si quieres que se ejecute solo al ocurrir el evento.
// Correcto<button onClick={manejarClick}>Guardar</button>
// Incorrecto para este caso: ejecuta la función durante el renderizado<button onClick={manejarClick()}>Guardar</button>Si necesitas pasar argumentos, usa una función flecha:
function ListaProductos({ productos }) { function seleccionarProducto(id) { console.log('Producto seleccionado:', id); }
return ( <ul> {productos.map((producto) => ( <li key={producto.id}> <button onClick={() => seleccionarProducto(producto.id)}> {producto.nombre} </button> </li> ))} </ul> );}Los eventos de formulario siguen usando preventDefault() cuando quieres evitar el envío tradicional.
11. Estado con useState
El estado permite guardar datos que cambian y provocan que la interfaz se actualice.
Para usar estado en React se emplea el hook useState.
import { useState } from 'react';
function Contador() { const [contador, setContador] = useState(0);
return ( <button onClick={() => setContador(contador + 1)}> Clics: {contador} </button> );}Partes importantes:
| Parte | Significado |
|---|---|
contador | Valor actual del estado. |
setContador | Función para solicitar un cambio de estado. |
useState(0) | Valor inicial. |
No modifiques el estado directamente. Usa siempre la función de actualización.
// No recomendadocontador = contador + 1;
// CorrectosetContador(contador + 1);Cuando cambia el estado, React vuelve a renderizar el componente y muestra la interfaz actualizada.
12. Actualizar estado a partir del estado anterior
Si el nuevo estado depende del estado anterior, es recomendable usar una función de actualización.
setContador((contadorActual) => contadorActual + 1);Esto es especialmente útil cuando hay varias actualizaciones seguidas o cuando trabajas con arrays y objetos.
Añadir un producto a un array:
const [productos, setProductos] = useState([]);
function añadirProducto(producto) { setProductos((productosActuales) => [ ...productosActuales, producto, ]);}Eliminar un producto:
function eliminarProducto(id) { setProductos((productosActuales) => productosActuales.filter((producto) => producto.id !== id), );}Observa que no usamos push() ni splice() directamente sobre el array del estado. Creamos un array nuevo. Esto ayuda a React a detectar el cambio y evita efectos secundarios.
13. Formularios controlados
Un formulario controlado es aquel cuyo valor está sincronizado con el estado de React.
Ejemplo con un campo:
import { useState } from 'react';
function FormularioNombre() { const [nombre, setNombre] = useState('');
return ( <form> <label htmlFor="nombre">Nombre</label> <input id="nombre" value={nombre} onChange={(event) => setNombre(event.target.value)} /> <p>Nombre escrito: {nombre}</p> </form> );}En este patrón:
- El valor del input viene del estado:
value={nombre}. - Cada cambio actualiza el estado:
onChange={...}. - La interfaz muestra siempre el valor actual.
Formulario con envío:
function FormularioProducto({ onAñadirProducto }) { const [nombre, setNombre] = useState('');
function manejarEnvio(event) { event.preventDefault();
onAñadirProducto({ id: Date.now(), nombre, });
setNombre(''); }
return ( <form onSubmit={manejarEnvio}> <label htmlFor="nombre-producto">Producto</label> <input id="nombre-producto" value={nombre} onChange={(event) => setNombre(event.target.value)} /> <button type="submit">Añadir</button> </form> );}Este enfoque permite validar, limpiar y transformar datos desde React.
14. Levantar el estado al componente padre
A veces varios componentes necesitan compartir los mismos datos. En ese caso, el estado debe estar en el componente padre común.
Ejemplo conceptual:
App├─ FormularioProducto└─ ListaProductosSi el formulario añade productos y la lista los muestra, el array de productos debe vivir en App, no dentro del formulario ni dentro de la lista.
function App() { const [productos, setProductos] = useState([]);
function añadirProducto(producto) { setProductos((productosActuales) => [ ...productosActuales, producto, ]); }
return ( <main> <FormularioProducto onAñadirProducto={añadirProducto} /> <ListaProductos productos={productos} /> </main> );}El padre guarda el estado y pasa:
- Datos hacia abajo mediante props.
- Funciones hacia abajo para permitir acciones.
Este patrón es muy habitual en React y será clave para construir aplicaciones pequeñas con componentes coordinados.
15. Estilos básicos en componentes
React no obliga a una única forma de escribir estilos. En proyectos iniciales con Vite puedes usar archivos CSS normales.
Ejemplo de importación en App.jsx:
import './App.css';Uso de clases en JSX:
function TarjetaProducto({ producto }) { return ( <article className="tarjeta-producto"> <h2>{producto.nombre}</h2> <p>{producto.precio} €</p> </article> );}CSS:
.tarjeta-producto { border: 1px solid #ddd; border-radius: 0.5rem; padding: 1rem;}También puedes aplicar clases condicionales:
function Stock({ disponible }) { const clase = disponible ? 'stock stock--ok' : 'stock stock--agotado';
return <p className={clase}>{disponible ? 'Disponible' : 'Agotado'}</p>;}Mantén la misma separación de responsabilidades que ya conoces: React decide qué clase se aplica; CSS define cómo se ve.
16. Conectar React con datos remotos
En esta unidad no profundizaremos todavía en efectos ni carga avanzada de datos. Eso se trabajará en React avanzado. Aun así, conviene entender cómo encaja lo que ya sabes de fetch.
En una aplicación React, los datos remotos suelen acabar en estado:
const [productos, setProductos] = useState([]);const [cargando, setCargando] = useState(false);const [error, setError] = useState(null);La interfaz se renderiza según esos estados:
function EstadoDatos({ cargando, error }) { if (cargando) { return <p>Cargando productos...</p>; }
if (error) { return <p>No se han podido cargar los productos.</p>; }
return null;}La idea importante es que React no cambia HTTP. Seguirás usando conceptos como:
fetch().response.ok.response.json().tryycatch.- Estados de carga y error.
Lo que cambia es dónde guardas el resultado y cómo lo muestras.
17. TypeScript como evolución natural
React puede usarse con JavaScript o con TypeScript. En esta unidad usaremos JavaScript para centrarnos en los conceptos de React, pero TypeScript es muy frecuente en proyectos profesionales.
TypeScript ayuda a expresar qué forma tienen las props, qué tipo de datos contiene el estado y qué espera cada función.
Ejemplo conceptual con TypeScript:
type Producto = { id: number; nombre: string; precio: number;};
function TarjetaProducto({ producto }: { producto: Producto }) { return <h2>{producto.nombre}</h2>;}No necesitas dominar TypeScript para empezar React, pero sí conviene verlo como una continuación natural. Puedes repasar TypeScript cuando quieras añadir tipado a tus componentes.
18. Proyecto guiado: gestor de productos
Construiremos una pequeña interfaz con tres componentes:
App├─ FormularioProducto└─ ListaProductos └─ ProductoItem18.1. Crear datos iniciales
En App.jsx:
import { useState } from 'react';import './App.css';
const productosIniciales = [ { id: 1, nombre: 'Teclado', precio: 29.99 }, { id: 2, nombre: 'Ratón', precio: 14.99 },];
function App() { const [productos, setProductos] = useState(productosIniciales);
return ( <main> <h1>Gestor de productos</h1> </main> );}
export default App;18.2. Crear el componente ProductoItem
function ProductoItem({ producto, onEliminar }) { return ( <li className="producto-item"> <span>{producto.nombre} - {producto.precio.toFixed(2)} €</span> <button type="button" onClick={() => onEliminar(producto.id)}> Eliminar </button> </li> );}Este componente recibe el producto y una función para eliminarlo. No sabe dónde vive el array de productos; solo muestra una fila y avisa cuando el usuario pulsa el botón.
18.3. Crear el componente ListaProductos
function ListaProductos({ productos, onEliminar }) { if (productos.length === 0) { return <p>No hay productos.</p>; }
return ( <ul className="lista-productos"> {productos.map((producto) => ( <ProductoItem key={producto.id} producto={producto} onEliminar={onEliminar} /> ))} </ul> );}Aquí aparecen tres ideas importantes: renderizado condicional, listas con map() y key estable.
18.4. Crear el componente FormularioProducto
function FormularioProducto({ onAñadir }) { const [nombre, setNombre] = useState(''); const [precio, setPrecio] = useState('');
function manejarEnvio(event) { event.preventDefault();
const nombreLimpio = nombre.trim(); const precioNumero = Number(precio);
if (nombreLimpio === '' || Number.isNaN(precioNumero)) { return; }
onAñadir({ id: Date.now(), nombre: nombreLimpio, precio: precioNumero, });
setNombre(''); setPrecio(''); }
return ( <form onSubmit={manejarEnvio} className="formulario-producto"> <label htmlFor="nombre">Nombre</label> <input id="nombre" value={nombre} onChange={(event) => setNombre(event.target.value)} />
<label htmlFor="precio">Precio</label> <input id="precio" type="number" min="0" step="0.01" value={precio} onChange={(event) => setPrecio(event.target.value)} />
<button type="submit">Añadir producto</button> </form> );}El precio se guarda inicialmente como texto porque los valores de los inputs llegan como texto. Se convierte a número al enviar.
18.5. Conectar todo en App
function App() { const [productos, setProductos] = useState(productosIniciales);
function añadirProducto(producto) { setProductos((productosActuales) => [ ...productosActuales, producto, ]); }
function eliminarProducto(id) { setProductos((productosActuales) => productosActuales.filter((producto) => producto.id !== id), ); }
return ( <main> <h1>Gestor de productos</h1> <FormularioProducto onAñadir={añadirProducto} /> <ListaProductos productos={productos} onEliminar={eliminarProducto} /> </main> );}Este proyecto resume la unidad:
Appguarda el estado compartido.FormularioProductorecoge datos y avisa al padre.ListaProductosrenderiza una lista.ProductoItemmuestra un producto y permite eliminarlo.- Las props conectan los componentes.
- El estado cambia sin manipular el DOM manualmente.
19. Buenas prácticas iniciales
- Usa componentes pequeños y con responsabilidad clara.
- Nombra los componentes con mayúscula inicial.
- No modifiques arrays u objetos de estado directamente.
- Usa
map()para renderizar listas. - Añade
keyestable en listas. - Mantén el estado en el componente que lo necesita o en el padre común.
- Usa formularios controlados cuando necesites sincronizar datos con React.
- Separa estilos en CSS cuando sea suficiente.
- Evita añadir librerías antes de entender el problema.
- Revisa la consola del navegador y los mensajes del servidor de desarrollo.
20. Ejercicios
Ejercicio 1: Crear proyecto React con Vite
Crea un proyecto React con Vite, instala dependencias, ejecuta el servidor de desarrollo y localiza los archivos main.jsx y App.jsx.
Ejercicio 2: Primer componente
Crea un componente Cabecera que muestre el título de una aplicación y un subtítulo. Úsalo dentro de App.
Ejercicio 3: Tarjeta con props
Crea un componente TarjetaAlumno que reciba nombre, curso y nota como props. Renderiza al menos tres tarjetas con datos distintos.
Ejercicio 4: Renderizado condicional
Crea un componente EstadoPedido que muestre mensajes distintos para pendiente, enviado y entregado.
Ejercicio 5: Lista con key
Crea un array de productos con id, nombre y precio. Renderiza la lista con map() y usa id como key.
Ejercicio 6: Contador con useState
Crea un contador con botones para sumar y restar. Evita que el contador baje de cero.
Ejercicio 7: Formulario controlado
Crea un formulario con un campo de nombre. Muestra debajo el texto que el usuario está escribiendo en tiempo real.
Ejercicio 8: Añadir elementos a una lista
Crea una lista de tareas en estado. Añade un formulario para incorporar nuevas tareas sin recargar la página.
Ejercicio 9: Eliminar elementos de una lista
Amplía el ejercicio anterior añadiendo un botón de eliminar en cada tarea. Usa filter() para actualizar el estado.
Ejercicio 10: Proyecto integrador
Construye el gestor de productos de la unidad y añade una mejora: contador de productos, mensaje cuando no hay productos o clase visual para productos de precio superior a 50 €.