React sigue siendo la librería de UI más usada del mundo, pero en 2025 el ecosistema ha madurado enormemente:
React 19 introduce acciones, hooks de formulario y mejoras masivas al modelo de concurrencia.
React Server Components permiten renderizar componentes en el servidor sin enviar JS al cliente.
El paradigma ha pasado de "SPA pura" a arquitecturas híbridas (streaming, partial hydration, islands).
Frameworks como Next.js 15, Remix y TanStack Start construyen sobre React para darte lo mejor de servidor y cliente.
¿Qué aprenderás aquí? No solo la API de React, sino cómo pensar en componentes, arquitectura escalable, rendimiento real y prácticas que usan equipos profesionales.
2. Entorno de desarrollo profesional
2.1 Requisitos
2.2 Crear un proyecto moderno
2.3 Configuración ESLint + Prettier profesional
.eslintrc.json:
.prettierrc:
2.4 Path aliases (tsconfig.json)
✅ Buena práctica: Usa siempre imports absolutos con alias. import { Button } from '@/components/ui' es mucho más mantenible que '../../../../components/ui'.
3. Fundamentos sólidos de React
3.1 El modelo mental correcto
React es una función de tu estado: UI = f(state). Cada vez que el estado cambia, React recalcula qué partes del DOM deben actualizarse. Interioriza esto antes de memorizar APIs.
// ❌ Nunca uses el índice como key en listas dinámicasitems.map((item, index) => <Item key={index} {...item} />)// ✅ Usa un identificador único y estableitems.map((item) => <Item key={item.id} {...item} />)
4. Hooks en profundidad
4.1 useState: cuándo y cómo
tsx
// ✅ Agrupar estado relacionado en un objetoconst [form, setForm] = useState({ name: '', email: '', role: 'user' as const,})// Actualizar campos individualmente sin perder los demásconst handleChange = (field: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement>) => { setForm(prev => ({ ...prev, [field]: e.target.value }))}
4.2 useEffect: las reglas de oro
tsx
// ❌ Efecto sin cleanup (memory leak)useEffect(() => { const subscription = subscribe(userId)}, [userId])// ✅ Siempre limpia tus efectosuseEffect(() => { const subscription = subscribe(userId) return () => subscription.unsubscribe()}, [userId])// ✅ Evita efectos para derivar estado — usa useMemoconst fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName])// NO hagas esto con useEffect + setState para el mismo resultado
🔑 Regla: Si puedes calcular algo durante el render, no uses useEffect. Los efectos son para sincronizar con sistemas externos (APIs, suscripciones, DOM manual).
// components/ui/index.tsexport { Button } from './Button/Button'export { Input } from './Input/Input'export { Modal } from './Modal/Modal'export type { ButtonProps } from './Button/Button'// Uso limpio en cualquier parte del proyectoimport { Button, Input, Modal } from '@/components/ui'
// En Next.js, siempre usa next/imageimport Image from 'next/image'function Avatar({ user }: { user: User }) { return ( <Image src={user.avatarUrl} alt={`Avatar de ${user.name}`} width={48} height={48} className="rounded-full" priority={false} // true solo para imágenes above the fold placeholder="blur" blurDataURL={user.avatarBlurHash} /> )}
Con Next.js App Router, los componentes son Server Components por defecto:
┌─────────────────────────────────────────────────────┐
│ Server Components │
│ ✅ Acceso directo a DB, filesystem, APIs internas │
│ ✅ No incluyen JS en el bundle del cliente │
│ ✅ Pueden ser async/await nativamente │
│ ❌ Sin useState, useEffect, event handlers │
└─────────────────────────────────────────────────────┘
↓ pasan datos a
┌─────────────────────────────────────────────────────┐
│ Client Components ('use client') │
│ ✅ Interactividad, estado, efectos │
│ ✅ Acceso a APIs del navegador │
│ ❌ No pueden ser async de la misma forma │
└─────────────────────────────────────────────────────┘
9.2 Componente servidor async
tsx
// app/dashboard/page.tsx (Server Component)async function DashboardPage() { // Consulta directa a DB, sin useEffect ni loading states const [user, stats] = await Promise.all([ getUser(), getStats(), ]) return ( <main> <h1>Hola, {user.name}</h1> {/* StatsChart es un Client Component que recibe datos del servidor */} <StatsChart data={stats} /> </main> )}
9.3 Streaming con Suspense
tsx
// app/dashboard/page.tsximport { Suspense } from 'react'export default function Page() { return ( <main> {/* El header se muestra inmediatamente */} <DashboardHeader /> {/* Los datos pesados se cargan en streaming */} <Suspense fallback={<StatsSkeleton />}> <StatsSection /> </Suspense> <Suspense fallback={<FeedSkeleton />}> <ActivityFeed /> </Suspense> </main> )}
10.2 Testing de componentes: la filosofía correcta
🎯 Principio: Testea el comportamiento, no la implementación. Tus tests deben parecerse a cómo un usuario usa tu app.
tsx
// components/ui/Button/Button.test.tsximport { render, screen } from '@testing-library/react'import userEvent from '@testing-library/user-event'import { Button } from './Button'describe('Button', () => { it('llama a onClick cuando el usuario hace click', async () => { const handleClick = vi.fn() render(<Button label="Guardar" onClick={handleClick} />) await userEvent.click(screen.getByRole('button', { name: /guardar/i })) expect(handleClick).toHaveBeenCalledOnce() }) it('está deshabilitado y muestra spinner cuando isLoading es true', () => { render(<Button label="Guardar" isLoading />) const button = screen.getByRole('button') expect(button).toBeDisabled() expect(button).toHaveAttribute('aria-busy', 'true') })})
10.3 Testing de custom hooks
tsx
import { renderHook, act } from '@testing-library/react'import { useLocalStorage } from './useLocalStorage'describe('useLocalStorage', () => { beforeEach(() => window.localStorage.clear()) it('devuelve el valor inicial cuando no hay datos guardados', () => { const { result } = renderHook(() => useLocalStorage('key', 'default')) expect(result.current[0]).toBe('default') }) it('persiste el valor en localStorage', () => { const { result } = renderHook(() => useLocalStorage('key', '')) act(() => result.current[1]('nuevo valor')) expect(localStorage.getItem('key')).toBe('"nuevo valor"') })})
La accesibilidad no es opcional. Es un requisito legal en muchos países y beneficia a todos los usuarios (teclado, lectores de pantalla, dispositivos táctiles, conexiones lentas).
11.2 Reglas fundamentales
tsx
// ❌ Mal: div clickable sin semántica<div onClick={handleDelete} className="icon-btn">🗑️</div>// ✅ Bien: botón con label accessible<button onClick={handleDelete} aria-label="Eliminar elemento" type="button"> <TrashIcon aria-hidden="true" /> <span className="sr-only">Eliminar</span></button>
// lib/env.ts — con Zod para validar en build timeimport { z } from 'zod'const envSchema = z.object({ NEXT_PUBLIC_API_URL: z.string().url(), DATABASE_URL: z.string().min(1), AUTH_SECRET: z.string().min(32),})export const env = envSchema.parse({ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, DATABASE_URL: process.env.DATABASE_URL, AUTH_SECRET: process.env.AUTH_SECRET,})// Si falta alguna variable, el build falla con un mensaje claro ✅
Recuerda: La habilidad más importante no es conocer todas las APIs de React, sino saber cuándo no usarlas. Escribe el código más simple que funcione, mide antes de optimizar, y testa el comportamiento no la implementación.
Tutorial actualizado para React 19, Next.js 15 y el ecosistema de 2025.
bash
node --version # >= 20 LTS recomendadopnpm --version # pnpm > npm/yarn por velocidad y eficiencia de disco
bash
# Con Next.js 15 (recomendado para proyectos reales)pnpm create next-app@latest mi-proyecto --typescript --tailwind --eslint --app# Con Vite (para SPAs o librerías)pnpm create vite@latest mi-proyecto -- --template react-tscd mi-proyecto && pnpm install