Fundamentos de React
23 ejemplos de componentes para empezar con React: 10 básicos y 13 intermedios.
Busca en todas las páginas de la documentación
23 ejemplos de componentes para empezar con React: 10 básicos y 13 intermedios.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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).
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>
);
}@/ es un atajo para la raíz de tu proyecto, así que @/components/hello-world apunta a components/hello-world.tsx<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
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:
Usa llaves {} para incrustar expresiones de JavaScript dentro del JSX.
function Greeting() {
const name = "Alice";
return <p>¡Bienvenida, {name}!</p>;
}{} crean un espacio para expresiones: puedes poner cualquier expresión de JavaScript dentroif o for) directamente dentro de las llavesRelacionado: JSX y TSX -- reglas de expresiones, fragmentos y compilación de JSX | Fundamentos de TypeScript + React -- tipado de variables y expresiones
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 JSXkey única para que React pueda rastrear qué elementos cambiaronRelacionado: Listas y keys -- estrategias de keys, listas anidadas, filtrado y errores comunes
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} />{ name, age } directamente en la lista de parámetros para un acceso limpioRelacionado: Componentes -- patrones de composición y la prop children | Tipado de props -- interfaces, props opcionales, uniones discriminadas
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>
);
}? : es la forma más común de renderizar condicionalmente en JSX&& para "mostrar o nada": {isLoggedIn && <p>¡Bienvenido!</p>}Relacionado: Renderizado condicional -- ternario,
&&, retorno temprano y cláusulas de guarda en profundidad
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>;
}onClick, no onclick)onClick={handleClick}, no onClick={handleClick()}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
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>
);
}useState devuelve un par: el valor actual y una función setteruseState(0) es el valor inicial, usado solo en el primer renderRelacionado: useState -- funciones actualizadoras, inicialización perezosa y agrupación (batching) de estado | Tipado de estado -- tipado de formas de estado complejas
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>
);
}value={name}onChange se dispara con cada pulsación de tecla y actualiza el estado con el nuevo valorRelacionado: 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
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>children es todo lo que pongas entre las etiquetas de apertura y cierre de un componenteReact.ReactNode acepta cadenas, números, elementos, arrays o nullRelacionado: Composición -- slots, componentes compuestos y patrones de layout | Componentes -- la prop children en profundidad
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>
);
}className en lugar de class (ya que class es una palabra reservada en JavaScript)backgroundColor, no background-color)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
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>
);
}useEffect ejecuta efectos secundarios: cosas que ocurren fuera del renderizado (llamadas a API, temporizadores, suscripciones)[userId] le indica a React que vuelva a ejecutar el efecto solo cuando cambie userId[] significa "ejecutar una vez al montar"; omitirlo por completo significa "ejecutar después de cada render"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
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>
);
}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
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>
);
}showData) y lo pasa hacia abajo como propshowRows: devolver null lo oculta por completopx-4 py-2), colores (bg-blue-600 text-white), bordes (rounded-md border) y estados interactivos (hover:bg-blue-700)last:border-0 elimina el borde de la última fila para un aspecto limpioRelacionado: 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
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>variant = "primary") hacen que las props sean opcionales con valores predeterminados sensatos"primary" | "secondary" | "danger" restringe la prop a opciones válidasRelacionado: 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
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>
);
}use y pueden llamar a otros hooksRelacionado: 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
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>
);
}useState{ ...prev, [field]: value } crea un nuevo objeto con un campo actualizadokeyof FormData garantiza que solo puedas pasar nombres de campo válidos: las erratas se convierten en errores de compilacióne.preventDefault() al enviar el formulario para evitar la recarga de página predeterminada del navegadorRelacionado: 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
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>
);
}async que se ejecutan solo en el servidor: no se envía JavaScript al navegador por ellosawait sobre los datos directamente en el cuerpo del componente; sin useEffect, sin indicadores de carga, sin cascada del lado del clientenext: { revalidate: 60 } almacena en caché la respuesta durante 60 segundos y luego la vuelve a obtener en la siguiente peticiónRelacionado: 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"
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>
);
}"use server" marca una función como Server Action: solo se ejecuta en el servidor, incluso cuando se invoca desde un formulario del cliente<form action={...}>; React serializa los datos del formulario e invoca la acción al enviarrevalidatePath("/todos") le indica a Next.js que purgue la página en caché para que la nueva tarea aparezca de inmediatofetch, sin análisis de JSON: la llamada de red la gestionan por completo React y Next.jsRelacionado: Server Actions -- seguridad, validación y manejo de errores | Form Actions -- el soporte de acciones de formulario de React 19 | Revalidación --
revalidatePathvs.revalidateTag
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 };
}
}useActionState envuelve una Server Action y devuelve [state, action, isPending] en una sola llamadaisPending cambia a true durante el envío y vuelve a false cuando la acción se resuelve; perfecto para deshabilitar botones y mostrar spinners(prevState, formData) y lo que devuelve pasa a ser el nuevo state"use server" al inicio de actions.ts marca 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 segurosFormData en el servidor antes de confiar en ellos: el cliente puede enviar cualquier cosa, así que typeof email !== "string" y las comprobaciones de forma corresponden a la propia acción"use client"), pero la acción subyacente sigue ejecutándose en el servidorRelacionado: 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
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>
);
}useOptimistic te da un estado temporal que se actualiza al instante y luego revierte si la acción falla(current) => current + 1 describe cómo aplicar la actualización optimista sobre el estado realsetLikes(next)), React reconcilia y el valor optimista se reemplazaRelacionado: useOptimistic -- patrones de reductor, múltiples actualizaciones pendientes y recuperación de errores | Server Actions -- combinar UI optimista con mutaciones
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>
);
}async que 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"Promise.all: el árbol simplemente se renderiza a medida que cada pieza está listaRelacionado: 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
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>
);
}ReadableStream con Content-Type: text/event-stream: el formato del protocolo SSE no son más que líneas como data: <carga>\n\nEventSource integrado del navegador se reconecta automáticamente ante caídas de red y entrega cada línea data: a onmessageuseEffect que llame a source.close(); de lo contrario, la conexión se filtra al desmontar o navegarRelacionado: Streaming -- streaming de HTML vs. streaming de datos | Server Actions -- cuándo mutar vs. suscribirse | useEffect -- funciones de limpieza y patrones de suscripción
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>;
}createContext crea un contenedor para valores compartidos; <ThemeContext.Provider value={...}> envuelve la parte del árbol que puede leerlouseContext(ThemeContext) para leer el valor; no hace falta pasar props por cada componente intermediouseTheme) y lanzar un error cuando falta el provider te da un error claro en lugar de un null silenciosoRelacionado: 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
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥