Saltar al contenido principal
Logo FPCode

React: fundamentos para aplicaciones cliente

Contenido

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

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

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 manualReact
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:

Terminal window
node --version
npm --version

Crear un proyecto React con Vite:

Terminal window
npm create vite@latest react-productos -- --template react

Después entra en la carpeta e instala dependencias:

Terminal window
cd react-productos
npm install

Arranca el servidor de desarrollo:

Terminal window
npm run dev

El 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.js

Archivos importantes:

ArchivoFunción
index.htmlDocumento HTML base donde se monta la aplicación.
src/main.jsxPunto de entrada JavaScript que conecta React con el DOM.
src/App.jsxComponente principal inicial de la aplicación.
package.jsonDependencias y scripts del proyecto.
vite.config.jsConfiguració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:

  • Saludo es 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 default permite 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:

ReglaEjemplo
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 id real 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:

ParteSignificado
contadorValor actual del estado.
setContadorFunció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 recomendado
contador = contador + 1;
// Correcto
setContador(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
└─ ListaProductos

Si 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().
  • try y catch.
  • 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
└─ ProductoItem

18.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:

  • App guarda el estado compartido.
  • FormularioProducto recoge datos y avisa al padre.
  • ListaProductos renderiza una lista.
  • ProductoItem muestra 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 key estable 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 €.