Fundamentos de React
23 ejemplos de componentes para empezar con React: 10 básicos y 13 intermedios.
Instalación rápida
Pon en marcha un nuevo proyecto de React en menos de un minuto:
# Crea una nueva app de Next.js (recomendado)
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npm run devTu app ya se está ejecutando en http://localhost:3000. Edita app/page.tsx para empezar a construir. Si el puerto 3000 ya está en uso, Next.js pasará automáticamente al siguiente puerto disponible (3001, 3002, etc.); revisa la salida de tu terminal para ver la URL real.
Otras opciones: También puedes usar
npm create vite@latest my-app -- --template react-tspara una configuración ligera de Vite + React, onpx create-react-app my-app --template typescriptpara el enfoque clásico de CRA (ya no recomendado para proyectos nuevos).
Ejemplos básicos
1. Hola Mundo
El componente de React más simple: una función que devuelve JSX.
// components/hello-world.tsx
function HelloWorld() {
return <h1>¡Hola, Mundo!</h1>;
}
export default HelloWorld;Para usar este componente, impórtalo en otro archivo:
// app/page.tsx
import HelloWorld from "@/components/hello-world";
export default function Home() {
return (
<main>
<HelloWorld />
</main>
);
}- Un componente de React no es más que una función de JavaScript que devuelve JSX
- El nombre de la función debe empezar con mayúscula (React usa esto para distinguir los componentes de las etiquetas HTML)
- El JSX se parece a HTML, pero por debajo se compila a JavaScript
- Exportas el componente desde su archivo y luego lo importas donde quieras usarlo
@/es un atajo para la raíz de tu proyecto, así que@/components/hello-worldapunta acomponents/hello-world.tsx- Usas el componente como una etiqueta HTML:
<HelloWorld />
Relacionado: Componentes -- patrones de composición, props, children | Sintaxis de componentes de función -- declaración vs. función flecha, exportación por defecto vs. nombrada | JSX y TSX -- la sintaxis que devuelven los componentes
¿Por qué TypeScript?
Todos los ejemplos de esta guía usan TypeScript (archivos .tsx) porque es el estándar de la industria para el desarrollo profesional con React. Estas son las razones:
- Detecta errores antes de la ejecución -- TypeScript señala incompatibilidades de tipos, erratas y props faltantes en tiempo de compilación, para que nunca lleguen a tus usuarios.
- Código autodocumentado -- Las anotaciones de tipos dejan inmediatamente claras las props de los componentes y las firmas de las funciones a cualquiera que lea el código.
- Mejor soporte del editor -- Obtienes autocompletado, resaltado de errores en línea y herramientas de refactorización fiables que el JavaScript plano no puede ofrecer.
- Refactorización más segura a escala -- Renombrar una prop o cambiar la forma de un dato te muestra todos los archivos que necesitan actualizarse, en todo el código base.
- Expectativa de la industria -- Casi todos los códigos base profesionales de React y las ofertas de empleo requieren TypeScript; aprenderlo desde el primer día evita tener que reaprender después.
2. Mostrar una variable
Usa llaves {} para incrustar expresiones de JavaScript dentro del JSX.
function Greeting() {
const name = "Alice";
return <p>¡Bienvenida, {name}!</p>;
}- Las llaves
{}crean un espacio para expresiones: puedes poner cualquier expresión de JavaScript dentro - Esto funciona con cadenas, números y cualquier expresión que produzca un valor
- No puedes poner sentencias (como
ifofor) directamente dentro de las llaves
Relacionado: JSX y TSX -- reglas de expresiones, fragmentos y compilación de JSX | Fundamentos de TypeScript + React -- tipado de variables y expresiones
3. Renderizar una lista
Recorre un array con map para producir múltiples elementos.
function FruitList() {
const fruits = ["Manzana", "Plátano", "Cereza"];
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
}.map()transforma cada elemento del array en un elemento JSX- Cada elemento de la lista necesita una prop
keyúnica para que React pueda rastrear qué elementos cambiaron - Las keys deben ser identificadores estables: los índices del array funcionan, pero los IDs provenientes de los datos son mejores
Relacionado: Listas y keys -- estrategias de keys, listas anidadas, filtrado y errores comunes
4. Recibir props
Las props te permiten pasar datos de un componente padre a uno hijo.
function UserCard({ name, age }: { name: string; age: number }) {
return (
<div>
<h2>{name}</h2>
<p>Edad: {age}</p>
</div>
);
}
// Uso: <UserCard name="Bob" age={25} />- Las props son las entradas de tu componente; piensa en ellas como argumentos de una función
- Desestructuramos
{ name, age }directamente en la lista de parámetros para un acceso limpio - Las props son de solo lectura: un componente nunca debe modificar sus propias props
Relacionado: Componentes -- patrones de composición y la prop children | Tipado de props -- interfaces, props opcionales, uniones discriminadas
5. Renderizado condicional
Muestra u oculta contenido según una condición.
function LoginStatus({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<div>
{isLoggedIn ? <p>¡Bienvenido de nuevo!</p> : <p>Por favor, inicia sesión.</p>}
</div>
);
}- El operador ternario
? :es la forma más común de renderizar condicionalmente en JSX - También puedes usar
&¶ "mostrar o nada":{isLoggedIn && <p>¡Bienvenido!</p>} - Para condiciones complejas, extrae la lógica en una variable antes del return
Relacionado: Renderizado condicional -- ternario,
&&, retorno temprano y cláusulas de guarda en profundidad
6. Manejar un evento de clic
Adjunta un manejador de eventos para responder a las acciones del usuario.
function ClickCounter() {
const handleClick = () => {
alert("¡Botón pulsado!");
};
return <button onClick={handleClick}>Haz clic en mí</button>;
}- Los nombres de eventos en JSX usan camelCase (
onClick, noonclick) - Pasa una referencia a la función, no una llamada a la función:
onClick={handleClick}, noonClick={handleClick()} - El manejador recibe un objeto de evento sintético que funciona de forma idéntica en todos los navegadores
Relacionado: Eventos -- eventos sintéticos, delegación de eventos y patrones de manejadores | Eventos de ratón -- clic, doble clic, hover | Tipado de eventos -- tipos de manejadores de eventos
7. Usar useState
Añade estado interactivo a un componente.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Cuenta: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}useStatedevuelve un par: el valor actual y una función setter- Llamar al setter provoca un re-render con el nuevo valor
- El argumento de
useState(0)es el valor inicial, usado solo en el primer render
Relacionado: useState -- funciones actualizadoras, inicialización perezosa y agrupación (batching) de estado | Tipado de estado -- tipado de formas de estado complejas
8. Entrada de texto
Captura lo que el usuario escribe con un input controlado.
import { useState } from "react";
function NameInput() {
const [name, setName] = useState("");
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Tu nombre"
/>
<p>¡Hola, {name || "desconocido"}!</p>
</div>
);
}- Un input controlado mantiene su valor en el estado de React:
value={name} onChangese dispara con cada pulsación de tecla y actualiza el estado con el nuevo valor- Esto te da control total sobre el input: puedes validarlo, transformarlo o restablecerlo en cualquier momento
Relacionado: Formularios -- inputs controlados vs. no controlados, envío de formularios | Controlado vs. no controlado -- cuándo usar cada uno | Eventos de formulario -- onChange, onSubmit, onBlur
9. Renderizar children
Usa la prop especial children para envolver contenido.
function Card({ children }: { children: React.ReactNode }) {
return <div className="border rounded p-4 shadow">{children}</div>;
}
// Uso:
// <Card>
// <h2>Título</h2>
// <p>Algo de contenido aquí.</p>
// </Card>childrenes todo lo que pongas entre las etiquetas de apertura y cierre de un componente- El tipo
React.ReactNodeacepta cadenas, números, elementos, arrays onull - Este patrón es esencial para construir componentes de layout y envoltura
Relacionado: Composición -- slots, componentes compuestos y patrones de layout | Componentes -- la prop children en profundidad
10. Aplicar estilos
Añade clases CSS o estilos en línea a los elementos.
function StyledBox() {
return (
<div
className="container"
style={{ backgroundColor: "lightblue", padding: "1rem" }}
>
<p>Estilizado con className y estilos en línea a la vez.</p>
</div>
);
}- Usa
classNameen lugar declass(ya queclasses una palabra reservada en JavaScript) - Los estilos en línea usan un objeto de JavaScript con propiedades en camelCase (
backgroundColor, nobackground-color) - En la práctica, la mayoría de los proyectos usan un framework de CSS como Tailwind en lugar de estilos en línea
Relacionado: Configuración de Tailwind -- configurar Tailwind en un proyecto de Next.js | Utilidades de Tailwind -- clases de espaciado, tipografía y layout | Modo oscuro -- cambio de tema con Tailwind
Ejemplos intermedios
11. Obtener datos con useEffect
Carga datos de una API cuando el componente se monta.
import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then((res) => res.json())
.then((data) => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <p>Cargando...</p>;
if (!user) return <p>Usuario no encontrado.</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}useEffectejecuta efectos secundarios: cosas que ocurren fuera del renderizado (llamadas a API, temporizadores, suscripciones)- El array de dependencias
[userId]le indica a React que vuelva a ejecutar el efecto solo cuando cambieuserId - Un array vacío
[]significa "ejecutar una vez al montar"; omitirlo por completo significa "ejecutar después de cada render" - Maneja siempre los estados de carga y de error/vacío para que la UI nunca quede en blanco
Relacionado: useEffect -- limpieza, arrays de dependencias y errores comunes | Obtención básica con SWR -- un mejor enfoque para obtener datos con caché y revalidación | Suspense -- estados de carga con React Suspense
12. Elevar el estado
Comparte estado entre componentes hermanos moviéndolo a su padre.
import { useState } from "react";
function TemperatureInput({
label,
value,
onChange,
}: {
label: string;
value: string;
onChange: (val: string) => void;
}) {
return (
<label>
{label}:{" "}
<input value={value} onChange={(e) => onChange(e.target.value)} />
</label>
);
}
function TemperatureConverter() {
const [celsius, setCelsius] = useState("");
const fahrenheit = celsius ? String((parseFloat(celsius) * 9) / 5 + 32) : "";
return (
<div>
<TemperatureInput label="Celsius" value={celsius} onChange={setCelsius} />
<p>Fahrenheit: {fahrenheit || "--"}</p>
</div>
);
}- Cuando dos componentes necesitan los mismos datos, mueve ese estado a su ancestro común más cercano
- El padre pasa el valor hacia abajo como prop y una función setter para que el hijo la invoque
- Esto mantiene una única fuente de verdad: el estado vive en un solo lugar y fluye hacia abajo
- Los valores derivados (como la conversión a Fahrenheit) se calculan durante el render, no se almacenan en un estado separado
Relacionado: Patrones de Context -- compartir estado sin prop drilling | Configuración de Zustand -- cuando elevar el estado no basta, usa un store | Context vs. Zustand -- elegir entre Context y Zustand
13. Alternar la visibilidad del hijo desde el padre
Controla la visibilidad de un componente hijo desde un botón del padre usando estado y props.
import { useState } from "react";
interface DataRowsProps {
showRows: boolean;
data: string[];
}
function DataRows({ showRows, data }: DataRowsProps) {
if (!showRows) return null;
return (
<div className="mt-4 rounded-lg border border-gray-200 bg-white shadow-sm">
{data.map((item, index) => (
<div
key={index}
className="border-b border-gray-100 px-4 py-3 last:border-0 hover:bg-gray-50"
>
{item}
</div>
))}
</div>
);
}
function Dashboard() {
const [showData, setShowData] = useState(false);
const items = ["Fila 1: Datos de usuario", "Fila 2: Analíticas", "Fila 3: Informes"];
return (
<div className="p-6">
<button
onClick={() => setShowData(!showData)}
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{showData ? "Ocultar" : "Mostrar"} detalles
</button>
<DataRows showRows={showData} data={items} />
</div>
);
}- El padre mantiene el estado de visibilidad (
showData) y lo pasa hacia abajo como prop - El componente hijo renderiza condicionalmente según la prop
showRows: devolvernulllo oculta por completo - El botón de alternancia vive en el padre, dando un control centralizado sobre lo que se muestra
- Las clases utilitarias de Tailwind se encargan de todo el estilizado: espaciado (
px-4 py-2), colores (bg-blue-600 text-white), bordes (rounded-md border) y estados interactivos (hover:bg-blue-700) last:border-0elimina el borde de la última fila para un aspecto limpio- Este patrón escala bien: el padre puede controlar múltiples hijos o coordinar estados complejos de la UI
Relacionado: Renderizado condicional -- retorno temprano, guardas y patrones de visibilidad | Elevar el estado (Ejemplo 12) -- compartir estado entre hermanos | Composición -- construir jerarquías flexibles de componentes
14. Componente reutilizable con variantes
Construye un componente flexible usando props para controlar su apariencia.
function Button({
variant = "primary",
children,
onClick,
}: {
variant?: "primary" | "secondary" | "danger";
children: React.ReactNode;
onClick?: () => void;
}) {
const styles: Record<string, string> = {
primary: "bg-blue-600 text-white",
secondary: "bg-gray-200 text-gray-800",
danger: "bg-red-600 text-white",
};
return (
<button className={`px-4 py-2 rounded ${styles[variant]}`} onClick={onClick}>
{children}
</button>
);
}
// Uso:
// <Button variant="danger" onClick={handleDelete}>Eliminar</Button>- Los valores de parámetro por defecto (
variant = "primary") hacen que las props sean opcionales con valores predeterminados sensatos - Un tipo de unión
"primary" | "secondary" | "danger"restringe la prop a opciones válidas - Usar un objeto de búsqueda para los estilos mantiene el componente limpio y fácil de extender
- Este patrón escala bien: añade nuevas variantes agregando una sola entrada al objeto
Relacionado: Componentes compuestos -- APIs de componentes de varias partes | Componente Button -- un botón con variantes listo para producción | Uniones discriminadas -- props de variante con seguridad de tipos
15. Hook personalizado
Extrae lógica reutilizable en un hook personalizado.
import { useState } from "react";
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue((v) => !v);
const setOn = () => setValue(true);
const setOff = () => setValue(false);
return { value, toggle, setOn, setOff };
}
// Uso en un componente:
function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
const { value: isOpen, toggle } = useToggle();
return (
<div>
<button onClick={toggle}>
{title} {isOpen ? "▲" : "▼"}
</button>
{isOpen && <div>{children}</div>}
</div>
);
}- Los hooks personalizados son funciones que empiezan por
usey pueden llamar a otros hooks - Te permiten extraer y reutilizar lógica con estado sin cambiar el árbol de componentes
- El hook encapsula el estado y el comportamiento; el componente solo consume la API
- Devolver un objeto (en lugar de un array) permite a los consumidores renombrar fácilmente los valores desestructurados
Relacionado: Guía de hooks personalizados -- reglas, pruebas y patrones para hooks personalizados | useToggle -- el hook de alternancia usado en este ejemplo | Hooks personalizados (React Hooks) -- cuándo y por qué extraer hooks
16. Formulario con múltiples campos
Gestiona un formulario con varios inputs usando un único objeto de estado.
import { useState, type FormEvent } from "react";
interface FormData {
name: string;
email: string;
message: string;
}
function ContactForm() {
const [form, setForm] = useState<FormData>({ name: "", email: "", message: "" });
const handleChange = (field: keyof FormData, value: string) => {
setForm((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
console.log("Enviado:", form);
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="Nombre"
value={form.name}
onChange={(e) => handleChange("name", e.target.value)}
/>
<input
placeholder="Correo electrónico"
type="email"
value={form.email}
onChange={(e) => handleChange("email", e.target.value)}
/>
<textarea
placeholder="Mensaje"
value={form.message}
onChange={(e) => handleChange("message", e.target.value)}
/>
<button type="submit">Enviar</button>
</form>
);
}- Agrupa los inputs relacionados en un único objeto de estado en lugar de llamadas separadas a
useState - El operador de propagación
{ ...prev, [field]: value }crea un nuevo objeto con un campo actualizado keyof FormDatagarantiza que solo puedas pasar nombres de campo válidos: las erratas se convierten en errores de compilación- Llama siempre a
e.preventDefault()al enviar el formulario para evitar la recarga de página predeterminada del navegador
Relacionado: Patrones de formularios (complejos) -- formularios de varios pasos y campos dinámicos | React Hook Form + Zod -- formularios validados por esquema a escala | Checklist de decisión de formularios -- elegir la estrategia de formulario adecuada | Checklist de decisión de formularios con Gherkin -- planificación de formularios guiada por pruebas
17. Obtener datos en un Server Component
Omite useEffect por completo: obtén los datos en el servidor y renderiza HTML listo.
// app/users/page.tsx
interface User {
id: number;
name: string;
email: string;
}
export default async function UsersPage() {
const res = await fetch("https://jsonplaceholder.typicode.com/users", {
next: { revalidate: 60 },
});
const users: User[] = await res.json();
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} -- {user.email}
</li>
))}
</ul>
);
}- Los Server Components son funciones
asyncque se ejecutan solo en el servidor: no se envía JavaScript al navegador por ellos - Puedes usar
awaitsobre los datos directamente en el cuerpo del componente; sinuseEffect, sin indicadores de carga, sin cascada del lado del cliente - La opción
next: { revalidate: 60 }almacena en caché la respuesta durante 60 segundos y luego la vuelve a obtener en la siguiente petición - Los datos nunca pasan por el cliente, así que las claves de API y las credenciales de base de datos permanecen seguras en el servidor
Relacionado: Server Components -- cuándo usar componentes de servidor vs. de cliente | Obtención de datos -- caché, revalidación y obtenciones en paralelo | Client Components -- cuándo necesitas
"use client"
18. Mutar datos con una Server Action
Gestiona los envíos de formularios en el servidor sin escribir una ruta de API.
// app/todos/page.tsx
import { revalidatePath } from "next/cache";
async function createTodo(formData: FormData) {
"use server";
const title = formData.get("title") as string;
await db.todo.create({ data: { title } });
revalidatePath("/todos");
}
export default function TodosPage() {
return (
<form action={createTodo}>
<input name="title" placeholder="Nueva tarea" required />
<button type="submit">Añadir</button>
</form>
);
}- La directiva
"use server"marca una función como Server Action: solo se ejecuta en el servidor, incluso cuando se invoca desde un formulario del cliente - Pasa la función directamente a
<form action={...}>; React serializa los datos del formulario e invoca la acción al enviar revalidatePath("/todos")le indica a Next.js que purgue la página en caché para que la nueva tarea aparezca de inmediato- Sin ruta de API, sin
fetch, sin análisis de JSON: la llamada de red la gestionan por completo React y Next.js
Relacionado: Server Actions -- seguridad, validación y manejo de errores | Form Actions -- el soporte de acciones de formulario de React 19 | Revalidación --
revalidatePathvs.revalidateTag
19. Estado del formulario con useActionState
Controla el estado pendiente y las respuestas del servidor desde una acción de formulario.
"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";
interface State {
message: string;
ok: boolean;
}
const initialState: State = { message: "", ok: false };
export function SubscribeForm() {
const [state, formAction, isPending] = useActionState(subscribe, initialState);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? "Suscribiendo..." : "Suscribirse"}
</button>
{state.message && (
<p className={state.ok ? "text-green-600" : "text-red-600"}>{state.message}</p>
)}
</form>
);
}Y el archivo de la server action correspondiente:
// app/subscribe/actions.ts
"use server";
interface State {
message: string;
ok: boolean;
}
export async function subscribe(_prev: State, formData: FormData): Promise<State> {
const email = formData.get("email");
if (typeof email !== "string" || !email.includes("@")) {
return { message: "Introduce una dirección de correo válida.", ok: false };
}
try {
await db.subscriber.create({ data: { email } });
return { message: `¡Suscrito ${email}!`, ok: true };
} catch (err) {
if (isUniqueConstraintError(err)) {
return { message: "Ya estás suscrito.", ok: false };
}
return { message: "Algo salió mal. Inténtalo de nuevo.", ok: false };
}
}useActionStateenvuelve una Server Action y devuelve[state, action, isPending]en una sola llamada- El indicador
isPendingcambia atruedurante el envío y vuelve afalsecuando la acción se resuelve; perfecto para deshabilitar botones y mostrar spinners - La acción recibe
(prevState, formData)y lo que devuelve pasa a ser el nuevostate - La directiva
"use server"al inicio deactions.tsmarca cada exportación como Server Action: se ejecutan solo en el servidor, así que las llamadas a la base de datos y los secretos permanecen seguros - Valida siempre los valores de
FormDataen el servidor antes de confiar en ellos: el cliente puede enviar cualquier cosa, así quetypeof email !== "string"y las comprobaciones de forma corresponden a la propia acción - Solo funciona dentro de Client Components (
"use client"), pero la acción subyacente sigue ejecutándose en el servidor
Relacionado: useActionState -- forma de retorno, manejo de errores y patrones de validación | Form Actions -- el contrato de acciones de formulario de React 19 | Server Actions -- validar FormData en el servidor
20. UI optimista con useOptimistic
Muestra el resultado al instante mientras el servidor se pone al día.
"use client";
import { useOptimistic, useState } from "react";
import { addLike } from "./actions";
export function LikeButton({ postId, initialLikes }: { postId: number; initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(current) => current + 1,
);
async function handleClick() {
addOptimisticLike(null);
const next = await addLike(postId);
setLikes(next);
}
return (
<button onClick={handleClick}>
♥ {optimisticLikes}
</button>
);
}useOptimisticte da un estado temporal que se actualiza al instante y luego revierte si la acción falla- El reductor
(current) => current + 1describe cómo aplicar la actualización optimista sobre el estado real - Cuando llega la respuesta real del servidor (
setLikes(next)), React reconcilia y el valor optimista se reemplaza - Si la acción lanza un error, el valor optimista se revierte automáticamente; sin limpieza manual
Relacionado: useOptimistic -- patrones de reductor, múltiples actualizaciones pendientes y recuperación de errores | Server Actions -- combinar UI optimista con mutaciones
21. Streaming con Suspense
Transmite partes de la página a medida que sus datos están listos.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { RecentOrders } from "./recent-orders";
import { SalesChart } from "./sales-chart";
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<p>Cargando pedidos...</p>}>
<RecentOrders />
</Suspense>
<Suspense fallback={<p>Cargando gráfico...</p>}>
<SalesChart />
</Suspense>
</div>
);
}- Cada hijo es un Server Component
asyncque obtiene sus propios datos: el lento no bloquea al rápido <Suspense fallback={...}>le dice a React: "muestra este marcador de posición hasta que el hijo se resuelva"- Next.js transmite el HTML en fragmentos, así que los usuarios ven el contenido aparecer progresivamente en lugar de mirar una página en blanco
- Sin máquinas de estado de carga, sin coreografía de
Promise.all: el árbol simplemente se renderiza a medida que cada pieza está lista
Relacionado: Suspense -- límites, suspense anidado y manejo de errores | Streaming -- cómo Next.js transmite HTML | Suspense + Streaming (rendimiento) -- medir y optimizar el rendimiento del streaming | UI de carga y error --
loading.tsxa nivel de ruta
22. Transmitir eventos con Server-Sent Events
Envía actualizaciones en vivo desde el servidor al navegador a través de una única conexión HTTP.
// app/api/ticker/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
const data = JSON.stringify({ tick: i, time: Date.now() });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
await new Promise((r) => setTimeout(r, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}// app/ticker/page.tsx
"use client";
import { useEffect, useState } from "react";
interface Tick {
tick: number;
time: number;
}
export default function TickerPage() {
const [ticks, setTicks] = useState<Tick[]>([]);
useEffect(() => {
const source = new EventSource("/api/ticker");
source.onmessage = (e) => {
setTicks((prev) => [...prev, JSON.parse(e.data)]);
};
return () => source.close();
}, []);
return (
<ul>
{ticks.map((t) => (
<li key={t.tick}>Tick {t.tick} a las {new Date(t.time).toLocaleTimeString()}</li>
))}
</ul>
);
}- Un Route Handler devuelve un
ReadableStreamconContent-Type: text/event-stream: el formato del protocolo SSE no son más que líneas comodata: <carga>\n\n - El
EventSourceintegrado del navegador se reconecta automáticamente ante caídas de red y entrega cada líneadata:aonmessage - A diferencia de los WebSockets, SSE funciona sobre HTTP/2 plano, atraviesa la mayoría de los proxies y no necesita ninguna infraestructura adicional
- Devuelve siempre una función de limpieza desde
useEffectque llame asource.close(); de lo contrario, la conexión se filtra al desmontar o navegar
Relacionado: Streaming -- streaming de HTML vs. streaming de datos | Server Actions -- cuándo mutar vs. suscribirse | useEffect -- funciones de limpieza y patrones de suscripción
23. Compartir estado con useContext
Pasa datos a través del árbol sin prop drilling.
"use client";
import { createContext, useContext, useState, type ReactNode } from "react";
type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme debe usarse dentro de <ThemeProvider>");
return ctx;
}
// Uso en cualquier componente anidado:
function ThemeToggle() {
const { theme, toggle } = useTheme();
return <button onClick={toggle}>Tema actual: {theme}</button>;
}createContextcrea un contenedor para valores compartidos;<ThemeContext.Provider value={...}>envuelve la parte del árbol que puede leerlo- Cualquier descendiente llama a
useContext(ThemeContext)para leer el valor; no hace falta pasar props por cada componente intermedio - Envolver el hook (
useTheme) y lanzar un error cuando falta el provider te da un error claro en lugar de unnullsilencioso - Context vuelve a renderizar todos los consumidores cada vez que cambia el valor, así que mantén los valores del contexto pequeños y estables; divide los datos no relacionados en contextos separados
Relacionado: useContext -- patrones de provider, rendimiento y errores comunes | Patrones de Context -- separar estado vs. dispatch, providers compuestos | Context vs. Zustand -- cuándo recurrir a un store en su lugar