Fundamentos de React: bloques de construcción de UI moderna
BorradorURL: https://ruvebal.github.io/web-atelier-udit/lessons/es/react/react-fundamentals/
📋 Tabla de contenidos
- 🎯 Objetivo de la sesión (2 horas)
- 🧭 Requisitos previos
- 🚀 Configuración (10 minutos)
- 📦 La aplicación de tareas completa
- 🧱 Construyendo bloque por bloque
- 🔑 Profundización: Por qué importan las keys
- 🧠 Entendiendo la gestión de estado
- 🎨 Entendiendo la componentización
- 💭 Preguntas de reflexión
- 🧪 Ejercicios (Opcional)
- 📚 Conclusiones clave
- 🔗 Próximos pasos
- 🧘 Koan
- 📖 Lecturas adicionales
“Un componente es un compromiso: dados estos props, renderizaré esta UI.”
🎯 Objetivo de la sesión (2 horas)
Al final de esta sesión: Construir una aplicación de lista de tareas que demuestre:
- Componentes: Dividir la UI en piezas reutilizables
- Estado: Hacer que tu aplicación recuerde y actualice datos
- Keys: Ayudar a React a rastrear elementos de lista eficientemente
Lo que construirás: Una aplicación de tareas donde puedes añadir, eliminar y marcar tareas como completadas.
🧭 Requisitos previos
- Conocimientos básicos de HTML/CSS/JavaScript
- Node.js instalado (para Vite)
- Un editor de código (VS Code recomendado)
- Tailwind CSS (lo configuraremos juntos)
🚀 Configuración (10 minutos)
Paso 1: Crear proyecto React con Vite
# Crear nuevo proyecto
npm create vite@latest my-app-repo -- --template react
# Navegar al proyecto
cd my-app-repo
# Instalar dependencias
npm install
# Instalar Tailwind CSS v4 using Vite
npm install tailwindcss @tailwindcss/vite
Paso 2: Configurar Vite para Tailwind v4
Edita vite.config.js:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
Paso 3: Añadir Tailwind al CSS
Edita src/index.css (reemplaza todo el contenido):
@import 'tailwindcss';
Paso 4: Crear directorio de componentes
mkdir src/components
Paso 5: Iniciar servidor de desarrollo
npm run dev
Visita http://localhost:5173 — deberías ver la pantalla de bienvenida de Vite.
📦 La aplicación de tareas completa
Estructura de archivos final
src/
├── components/
│ ├── TaskList.jsx (muestra todas las tareas)
│ ├── TaskItem.jsx (componente de tarea individual)
│ └── AddTaskInput.jsx (input para añadir nuevas tareas)
├── App.jsx (componente principal con estado)
└── main.jsx (punto de entrada)
Crear directorio de componentes
mkdir src/components
🧱 Construyendo bloque por bloque
1. Entendiendo los componentes (Concepto)
¿Qué es un componente?
Un componente es una función que devuelve JSX (sintaxis similar a HTML). Piensa en él como una etiqueta HTML personalizada.
// Esto es un componente
function Greeting() {
return <h1>¡Hola, Mundo!</h1>;
}
// Lo usas así:
<Greeting />;
¿Por qué componentes?
- Reutilización: Escribe una vez, usa muchas veces
- Organización: Cada componente tiene un trabajo
- Mantenibilidad: Fácil de encontrar y corregir errores
2. El componente App principal
Archivo: src/App.jsx
Este es el “cerebro” de nuestra aplicación. Contiene el estado (los datos) y los pasa a los componentes hijos.
import { useState } from 'react';
import TaskList from './components/TaskList';
import AddTaskInput from './components/AddTaskInput';
function App() {
// ESTADO: La lista de tareas (esta es la memoria de nuestra app)
const [tasks, setTasks] = useState([
{ id: 1, text: 'Aprender fundamentos de React', completed: false },
{ id: 2, text: 'Construir una app de tareas', completed: false },
]);
// FUNCIÓN: Añadir una nueva tarea
const addTask = (text) => {
const newTask = {
id: Date.now(), // ID único simple
text: text,
completed: false,
};
setTasks([...tasks, newTask]); // Añadir a las tareas existentes
};
// FUNCIÓN: Eliminar una tarea
const removeTask = (id) => {
setTasks(tasks.filter((task) => task.id !== id));
};
// FUNCIÓN: Alternar completado de tarea
const toggleTask = (id) => {
setTasks(tasks.map((task) => (task.id === id ? { ...task, completed: !task.completed } : task)));
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-8 px-4">
<div className="max-w-2xl mx-auto">
<h1 className="text-4xl font-bold text-center text-indigo-900 mb-8">📝 Mi lista de tareas</h1>
<AddTaskInput onAdd={addTask} />
<TaskList tasks={tasks} onRemove={removeTask} onToggle={toggleTask} />
<div className="mt-6 text-center text-sm text-gray-600">
Total: {tasks.length} tareas | Completadas: {tasks.filter((t) => t.completed).length}
</div>
</div>
</div>
);
}
export default App;
🔑 Conceptos clave aquí:
useState: Crea una pieza de estado (datos que pueden cambiar)tasks= valor actualsetTasks= función para actualizarlo
- Las actualizaciones de estado son inmutables: Creamos nuevos arrays, no modificamos los existentes
- Props hacia abajo, eventos hacia arriba: Pasamos datos hacia abajo y funciones hacia abajo (para ser llamadas por los hijos)
📚 Métodos de arrays de JavaScript utilizados:
- Operador spread (
...): Crea un nuevo array/objeto copiando valores existentes[...tasks, newTask]→ copia todas las tareas, luego añade una nueva{ ...task, completed: !task.completed }→ copia el objeto tarea, actualiza una propiedad- MDN: Sintaxis spread
.filter(): Crea un nuevo array con elementos que pasan una pruebatasks.filter(task => task.id !== id)→ mantiene solo las tareas que no coinciden con el IDtasks.filter(t => t.completed)→ mantiene solo las tareas completadas- MDN: Array.filter()
.map(): Crea un nuevo array transformando cada elementotasks.map(task => ...)→ crea nuevo array, puede modificar cada tarea- Devuelve un array de la misma longitud, pero los elementos pueden cambiar
- MDN: Array.map()
3. El componente lista de tareas (Empezar simple: solo lectura)
Archivo: src/components/TaskList.jsx
Empecemos con el componente más simple: mostrar una lista. Sin edición todavía, solo mostrando tareas.
import TaskItem from './TaskItem';
function TaskList({ tasks, onRemove, onToggle }) {
// Si no hay tareas, mostrar un mensaje amigable
if (tasks.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
<p className="text-lg">¡Aún no hay tareas. Añade una arriba! 👆</p>
</div>
);
}
// Renderizar cada tarea usando .map()
return (
<div className="space-y-2">
{tasks.map((task) => (
<TaskItem key={task.id} task={task} onRemove={onRemove} onToggle={onToggle} />
))}
</div>
);
}
export default TaskList;
🔑 Conceptos clave aquí:
- Renderizado condicional: Mostrar diferente UI según el estado
if (tasks.length === 0)comprueba si el array está vacío- Devuelve diferente JSX según la condición
- Listas con
.map(): Transforma array en elementos Reacttasks.map(task => ...)transforma cada tarea en un componente<TaskItem />- Devuelve un array de elementos React que React renderiza
- MDN: Array.map()
- Keys: Cada
TaskItemnecesita un propkeyúnicokey={task.id}usa el ID único de la tarea- Ayuda a React a rastrear qué elementos cambiaron (más sobre esto abajo)
4. El componente input de añadir tarea (Simplificado: sin formulario)
Archivo: src/components/AddTaskInput.jsx
Este componente maneja la entrada del usuario. Usaremos un input + botón simple (sin envoltorio <form>).
import { useState } from 'react';
function AddTaskInput({ onAdd }) {
const [inputValue, setInputValue] = useState('');
const handleAdd = () => {
if (inputValue.trim()) {
onAdd(inputValue); // Llamar función del padre
setInputValue(''); // Limpiar input
}
};
const handleKeyDown = (e) => {
// Permitir tecla Enter para añadir tarea (como haría un formulario)
if (e.key === 'Enter') {
handleAdd();
}
};
return (
<div className="mb-6 flex gap-2">
{/* onChange: Se dispara cada vez que el usuario escribe
e = objeto evento (contiene info sobre lo que pasó)
e.target = el elemento DOM que disparó el evento (este input)
e.target.value = el texto actual escrito en el input */}
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="¿Qué necesitas hacer?"
className="flex-1 px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button
onClick={handleAdd}
className="px-6 py-3 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition-colors">
Añadir
</button>
</div>
);
}
export default AddTaskInput;
🔑 Conceptos clave aquí:
- Input controlado: React controla el valor del input (no el DOM)
- El
valuedel input viene del estado (inputValue) - Cada pulsación de tecla actualiza el estado vía
onChange - React re-renderiza con el nuevo valor
- El
- Manejadores de eventos:
onChange,onClick, yonKeyDownonChange: Se dispara cada vez que el usuario escribee= el objeto evento (contiene información sobre lo que pasó)e.target= el elemento DOM que disparó el evento (el campo input)e.target.value= el texto actual en ese campo input(e) => setInputValue(e.target.value)lee el texto actual del input y actualiza el estado- Actualiza el estado inmediatamente, creando una conexión “en vivo”
- MDN: Event.target
- MDN: Evento onChange
onClick: Se dispara cuando se hace clic en el botón- Más simple que
onSubmit(no necesitapreventDefault()) - MDN: Evento click
- Más simple que
onKeyDown: Se dispara cuando se presiona una teclae.key === 'Enter'comprueba si se presionó Enter- Permite interacción solo con teclado (¡accesibilidad!)
- MDN: Evento keydown
- Props:
onAddes una función pasada desde el padre
5. El componente elemento de tarea (Simplificado: sin modo edición)
Archivo: src/components/TaskItem.jsx
Este componente representa una tarea individual. Lo mantenemos simple: solo mostrar, alternar y eliminar.
function TaskItem({ task, onRemove, onToggle }) {
return (
<div className="bg-white rounded-lg shadow-sm p-4 flex items-center gap-3 hover:shadow-md transition-shadow">
{/* Checkbox para alternar completado */}
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggle(task.id)}
className="w-5 h-5 text-indigo-600 rounded focus:ring-2 focus:ring-indigo-500"
/>
{/* Texto de tarea (tachado si está completada) */}
<span className={`flex-1 ${task.completed ? 'line-through text-gray-400' : 'text-gray-800'}`}>{task.text}</span>
{/* Botón eliminar */}
<button
onClick={() => onRemove(task.id)}
className="px-3 py-1 text-sm bg-red-500 text-white rounded hover:bg-red-600 transition-colors">
Eliminar
</button>
</div>
);
}
export default TaskItem;
🔑 Conceptos clave aquí:
- Componente simple: No necesita estado local (más simple para principiantes)
- Estilos condicionales: Usa template literals para aplicar diferentes clases
- Propagación de eventos: Los botones llaman funciones del padre vía props (
onToggle,onRemove)
🔑 Profundización: Por qué importan las keys
El problema sin keys
Cuando React renderiza una lista, necesita saber qué elementos cambiaron. Sin keys, React usa el índice del array, lo que causa errores:
// ❌ MAL: Usar índice como key
{
tasks.map((task, index) => <TaskItem key={index} task={task} />);
}
¿Qué sale mal?
- Eliminar elemento 2 → React piensa que el elemento 3 es ahora el elemento 2
- Reordenar elementos → React se confunde sobre qué componente es cuál
- El estado del input se mezcla entre elementos
La solución: Keys únicas
// ✅ BIEN: Usar ID único como key
{
tasks.map((task) => <TaskItem key={task.id} task={task} />);
}
Por qué funciona:
- Cada tarea tiene un
idestable y único - React puede rastrear qué elemento es cuál, incluso si cambia el orden
- El estado del componente (como
isEditing) permanece con el elemento correcto
Regla general: Usa un identificador único de tus datos, no el índice del array.
🧠 Entendiendo la gestión de estado
¿Qué es el estado?
El estado son datos que cambian con el tiempo. En nuestra app:
- Array
tasks(enApp.jsx) — compartido por múltiples componentes inputValue(enAddTaskForm.jsx) — local al formularioisEditing(enTaskItem.jsx) — local a cada tarea
¿Dónde debe vivir el estado?
Regla: Pon el estado en el ancestro común más bajo de los componentes que lo necesitan.
App (estado tasks aquí - tanto TaskList como stats lo necesitan)
├── AddTaskForm (inputValue aquí - solo el formulario lo necesita)
└── TaskList
└── TaskItem (isEditing aquí - solo este elemento lo necesita)
Cómo actualizar el estado
Nunca mutes el estado directamente:
// ❌ MAL
tasks.push(newTask);
setTasks(tasks);
// ✅ BIEN
setTasks([...tasks, newTask]);
¿Por qué? React compara el estado antiguo y nuevo por referencia. Si mutas, React piensa que nada cambió.
El operador spread (...) crea un nuevo array:
[...tasks, newTask]copia todas las tareas existentes en un nuevo array, luego añade la nueva tarea- Esto crea una nueva referencia, así React sabe que el estado cambió
- MDN: Sintaxis spread
🎨 Entendiendo la componentización
¿Por qué dividir en componentes?
Nuestra app tiene 4 componentes. Podríamos escribirlo todo en un archivo, pero:
- Responsabilidad única: Cada componente hace una cosa
App→ gestiona el estadoAddTaskForm→ maneja la entradaTaskList→ muestra la listaTaskItem→ muestra una tarea
-
Reutilización:
TaskItemse usa una vez por tarea -
Testabilidad: Fácil de probar cada pieza independientemente
- Mantenibilidad: ¿Error en la edición? Mira
TaskItem.
Props: La API del componente
Los props son cómo los componentes se comunican entre sí:
// El padre pasa datos hacia abajo
<TaskItem
task={task} // datos
onRemove={removeTask} // función
/>;
// El hijo recibe vía props
function TaskItem({ task, onRemove }) {
// Usa task.text, llama onRemove(task.id)
}
Los props son de solo lectura. El hijo no puede modificarlos.
💭 Preguntas de reflexión
💭 Pregunta 1: Ubicación del estado
¿Por qué está el estado
tasksenApp.jsxy no enTaskList.jsx?Pista: ¿Quién más necesita saber sobre las tareas?
💭 Pregunta 2: Keys
¿Qué pasaría si usaras
Math.random()como key en lugar detask.id?Pista: Las keys deben ser estables entre renderizados.
💭 Pregunta 3: Inmutabilidad
¿Por qué escribimos
setTasks([...tasks, newTask])en lugar detasks.push(newTask)?Pista: React necesita detectar cambios.
🧪 Ejercicios (Opcional)
Ejercicio 1: Añadir botón “Limpiar completadas”
Añade un botón que elimine todas las tareas completadas.
Pista:
const clearCompleted = () => {
setTasks(tasks.filter((task) => !task.completed));
};
Ejercicio 2: Añadir prioridad de tarea
Añade un campo priority (baja, media, alta) y colorea las tareas según la prioridad.
Ejercicio 3: Persistir en localStorage
Guarda las tareas en localStorage para que sobrevivan al refresco de página.
Pista: Usa useEffect para sincronizar el estado con localStorage.
📚 Conclusiones clave
Componentes
- Funciones que devuelven JSX
- Dividen la UI en piezas reutilizables
- Reciben datos vía props
Estado
- Datos que cambian con el tiempo
- Usa el hook
useState - Nunca mutes directamente — crea nuevos arrays/objetos
- Usa métodos de arrays:
.map(),.filter(), y operador spread...
Keys
- Deben ser únicas y estables
- Ayudan a React a rastrear elementos de lista
- Usa IDs de datos, no índice del array
Flujo de datos
- Los props fluyen hacia abajo (padre → hijo)
- Los eventos fluyen hacia arriba (hijo llama función del padre)
- El estado vive en el ancestro común
🔗 Próximos pasos
Después de dominar esto, estás listo para:
- Profundización en Hooks (
useEffect,useRef, hooks personalizados) - Arquitectura de estado (Context, reducers, Zustand)
- Enrutamiento (React Router para apps multipágina)
- Integración con backend (obtener datos de APIs)
🧘 Koan
“Un componente sin estado es una función pura. Un componente con estado es algo vivo.”