Conceptos básicos de React Patterns
11 ejemplos para iniciarte con React Patterns -- 7 básicos y 4 intermedios.
Requisitos previos
No requiere paquetes extra -- cada patrón en esta página viene con React. Un proyecto estándar de React (Next.js, Vite o CRA) con TypeScript es suficiente.
Los patrones a continuación son formas de pensar sobre componentes -- cómo se componen, cómo comparten estado, dónde colocar los límites de carga y error. No son APIs que instales; son formas reutilizables que reconoces.
Ejemplos básicos
1. Composición con children
Construye componentes de layout y wrapper aceptando children en lugar de hacer subclasing o configurar.
function Card({ children }: { children: React.ReactNode }) {
return <div className="border rounded p-4 shadow">{children}</div>;
}
// Uso
function Page() {
return (
<Card>
<h2>Título</h2>
<p>Cualquier contenido que el padre quiera pasar.</p>
</Card>
);
}childrenes lo que pones entre las etiquetas de apertura y cierre del componente.React.ReactNodeacepta strings, números, elementos, fragmentos onull-- el tipo "renderizable" más amplio.- Prefiere la composición sobre props-para-todo -- mantiene la API pequeña y permite que los consumidores controlen el layout.
- Cuando necesites "slots" nombrados (header + footer), acéptalos como props nombrados:
<Card header={...} footer={...} />.
Relacionado: Composición sobre herencia -- slots, componentes compuestos, patrones de layout | Componentes (Fundamentos) -- la prop
childrenen profundidad
2. Controlado vs no controlado
Decide quién es propietario del estado del componente: el padre (controlado) o el componente mismo (no controlado).
import { useState } from "react";
// Controlado: el padre es propietario del value
function ControlledInput({
value, onChange,
}: {
value: string;
onChange: (v: string) => void;
}) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
// No controlado: el componente posee su propio estado internamente
function UncontrolledInput({ defaultValue = "" }: { defaultValue?: string }) {
const [value, setValue] = useState(defaultValue);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}- Controlado permite que el padre valide, transforme o reinicie el value -- la opción correcta para formularios y asistentes.
- No controlado es más simple para widgets autónomos que nunca necesitan coordinación externa.
- Ofrece ambos en un componente reutilizable: acepta
value/onChange(controlado) odefaultValue(no controlado), nunca los mezcles. - Cambiar de modo durante el ciclo de vida genera una advertencia de "controlado a no controlado" -- elige uno en el montaje y cúmplelo.
Relacionado: Controlado vs no controlado -- forma de la API, componentes con ambos modos, casos límite | Formularios: Controlado vs no controlado -- el lado específico del formulario
3. Render Props
Comparte comportamiento pasando una función como child o prop, devolviendo JSX basado en el estado interno.
import { useState } from "react";
function Toggle({
render,
}: {
render: (state: { on: boolean; toggle: () => void }) => React.ReactNode;
}) {
const [on, setOn] = useState(false);
return <>{render({ on, toggle: () => setOn((v) => !v) })}</>;
}
// Uso
function App() {
return (
<Toggle
render={({ on, toggle }) => (
<button onClick={toggle}>{on ? "ON" : "OFF"}</button>
)}
/>
);
}- El render prop es una función; el componente compartido la llama con estado y helpers.
- Los consumidores deciden qué renderizar -- un componente, muchas presentaciones.
- Los custom hooks cubren la mayoría de casos que los render props solucionaban, pero los render props siguen brillando para APIs específicas de renderizado (posicionamiento, medición, librerías de movimiento).
- Nombra el prop
render,childreno algo descriptivo (renderItem); sé consistente dentro de un codebase.
Relacionado: Render Props -- cuándo seguir usándolos, alternativa con hooks | Componentes de orden superior -- un patrón hermano de indirección
4. Componentes compuestos
Diseña componentes multiparte que compartan estado implícito a través de context -- el padre coordina, los hijos se componen.
import { createContext, useContext, useState } from "react";
const TabsCtx = createContext<{ active: string; setActive: (v: string) => void; } | null>(null);
function Tabs({ defaultValue, children }: { defaultValue: string; children: React.ReactNode }) {
const [active, setActive] = useState(defaultValue);
return <TabsCtx.Provider value={{ active, setActive }}>{children}</TabsCtx.Provider>;
}
function Tab({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(TabsCtx)!;
return (
<button onClick={() => ctx.setActive(value)} aria-pressed={ctx.active === value}>
{children}
</button>
);
}
function Panel({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(TabsCtx)!;
return ctx.active === value ? <div>{children}</div> : null;
}
// Uso
// <Tabs defaultValue="a">
// <Tab value="a">A</Tab><Tab value="b">B</Tab>
// <Panel value="a">Contenido A</Panel><Panel value="b">Contenido B</Panel>
// </Tabs>- El padre es propietario del estado; los hijos leen a través de context -- el consumidor nunca pasa props manualmente.
- Exporta los sub-componentes bajo el namespace del padre (
Tabs.Tab,Tabs.Panel) para una API limpia. - Refuerza el límite del padre: lanza un error útil si
useContextdevuelvenull, para que el mal uso falle ruidosamente. - Esta es la base de los primitivos shadcn y Radix UI -- aprende una vez, reconoce en todos lados.
Relacionado: Componentes compuestos -- profundización, tipado TypeScript | Patrones de Context -- mejores prácticas de context
5. Patrón de Context
Comparte datos en un subárbol sin prop drilling; divide contexts por frecuencia de actualización para evitar sobre-renderizado.
import { createContext, useContext, useMemo, useState } from "react";
type Theme = "light" | "dark";
const ThemeCtx = createContext<Theme>("light");
const SetThemeCtx = createContext<(t: Theme) => void>(() => {});
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
const set = useMemo(() => setTheme, []);
return (
<ThemeCtx.Provider value={theme}>
<SetThemeCtx.Provider value={set}>{children}</SetThemeCtx.Provider>
</ThemeCtx.Provider>
);
}
function ThemeToggle() {
const theme = useContext(ThemeCtx);
const setTheme = useContext(SetThemeCtx);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
{theme}
</button>
);
}- Divide los contexts de lectura y escritura -- los componentes que solo llaman a
setThemeno re-renderizada cuandothemecambia. - Envuelve el setter en
useMemoouseCallbackpara que el value del context de escritura permanezca referencialmente estable. - Nunca pongas valores que cambien frecuentemente (posición del ratón, scroll) en context -- cada consumidor re-renderiza.
- Para estado del servidor (usuario, carrito, tema entre pestañas), recurre a Zustand o un enfoque sincronizado con URL.
Relacionado: Patrones de Context -- división, optimización, componentes del servidor | useContext -- el hook subyacente | Context vs. Zustand -- cuándo usar cada uno
6. Error Boundary
Captura errores de renderizado en un subárbol y renderiza un fallback en lugar de una pantalla blanca.
"use client";
import { Component, type ReactNode } from "react";
interface State { error: Error | null; }
class ErrorBoundary extends Component<{ children: ReactNode }, State> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(error: Error) {
console.error("Error de UI:", error);
}
render() {
if (this.state.error) {
return <p role="alert">Algo salió mal: {this.state.error.message}</p>;
}
return this.props.children;
}
}
// Uso
// <ErrorBoundary><BuggyChart /></ErrorBoundary>- Las Error Boundaries deben ser componentes de clase --
getDerivedStateFromErroraún no tiene equivalente con hooks. - Capturan errores durante renderizado, métodos del ciclo de vida y constructores -- no manejadores de eventos, código asincrónico o componentes del servidor.
- Colócalas en límites significativos: por ruta, por panel, por widget. Una gran Error Boundary en la parte superior oculta demasiado.
- Para aplicaciones en producción, usa
react-error-boundary(API más ligera y amigable con hooks) e inicia sesión en Sentry/Datadog encomponentDidCatch.
Relacionado: Error Boundaries -- react-error-boundary, Next.js
error.tsx, logging | Suspense -- el contraparte de carga
7. Portal
Renderiza children en un nodo DOM fuera del árbol del padre -- escapa de trampas de overflow: hidden y z-index.
"use client";
import { createPortal } from "react-dom";
function Modal({
open, onClose, children,
}: {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}) {
if (!open || typeof document === "undefined") return null;
return createPortal(
<div
onClick={onClose}
style={{
position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)",
display: "grid", placeItems: "center",
}}
>
<div onClick={(e) => e.stopPropagation()} style={{ background: "white", padding: 24 }}>
{children}
</div>
</div>,
document.body,
);
}createPortal(node, container)montanodedentro decontainer, pero los eventos aún burbujean hacia el padre React.- Perfecto para modales, tooltips y dropdowns -- se renderizen en la parte superior del DOM sin importar dónde los escribas.
- Protégete con
typeof document === "undefined"para que sea seguro en SSR; de lo contrario,documentlanza en el servidor. - Gestiona el foco: atrápalo en el modal al abrirse, restáuralo al abridor al cerrarse -- la accesibilidad depende de ello.
Relacionado: React Portals -- trampa de foco, accesibilidad, recetas de z-index | Componente Modal -- patrones de modal en producción
Ejemplos intermedios
8. Suspense para UI asincrónica
Muestra declarativamente un fallback mientras un child asincrónico se resuelve -- sin pasar props isLoading.
"use client";
import { Suspense, use } from "react";
interface User { id: number; name: string; }
function UserCard({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h2>{user.name}</h2>;
}
export default function UserPage({
userPromise,
}: {
userPromise: Promise<User>;
}) {
return (
<Suspense fallback={<p>Cargando usuario...</p>}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}<Suspense fallback={...}>captura componentes children que "suspenden" (Componentes del Servidor esperando datos, Componentes del Cliente leyendo una promise conuse).- El fallback se renderiza inmediatamente; el contenido real lo reemplaza cuando el trabajo asincrónico finaliza.
- Crea la promise en un padre (a menudo un Componente del Servidor) y pásala -- no la recrees en cada renderizado.
- Combina con una
<ErrorBoundary>para que las promises rechazadas muestren una UI de error en lugar de propagarse por el árbol.
Relacionado: Suspense Boundaries -- colocación, streaming, casos límite | hook use -- leyendo promises y context | Streaming de Next.js -- Suspense en Componentes del Servidor
9. Componente de orden superior
Envuelve un componente para inyectar comportamiento compartido -- autenticación, logging, feature flags.
"use client";
import { useEffect } from "react";
function withLogging<P extends object>(
Component: React.ComponentType<P>,
label: string
) {
return function LoggedComponent(props: P) {
useEffect(() => {
console.log(`[${label}] montado`);
return () => console.log(`[${label}] desmontado`);
}, []);
return <Component {...props} />;
};
}
// Uso
function Dashboard({ userId }: { userId: string }) {
return <p>Dashboard para {userId}</p>;
}
const LoggedDashboard = withLogging(Dashboard, "Dashboard");- Un HOC es una función que toma un componente y devuelve uno nuevo con comportamiento extra.
- Usa genéricos (
<P extends object>) para que el componente envuelto mantenga sus tipos de prop originales. - Los custom hooks generalmente reemplazan HOCs en React moderno -- recurre a HOCs solo cuando necesites inyectar JSX o envolver componentes de clase.
- Nombra el componente devuelto (
function LoggedComponent(...)) para que React DevTools muestre algo útil.
Relacionado: Componentes de orden superior -- tipado, gotchas, cuándo preferir hooks | Render Props -- el otro patrón de "indirección"
10. Máquina de estados para lógica de UI
Modela estados complejos de UI como transiciones explícitas -- sin más combinaciones "imposibles" de props.
import { useReducer } from "react";
type Status =
| { kind: "idle" }
| { kind: "loading" }
| { kind: "success"; data: string }
| { kind: "error"; message: string };
type Event =
| { type: "FETCH" }
| { type: "RESOLVE"; data: string }
| { type: "REJECT"; message: string }
| { type: "RESET" };
function reducer(state: Status, event: Event): Status {
switch (state.kind) {
case "idle": return event.type === "FETCH" ? { kind: "loading" } : state;
case "loading": return event.type === "RESOLVE" ? { kind: "success", data: event.data }
: event.type === "REJECT" ? { kind: "error", message: event.message }
: state;
case "success":
case "error": return event.type === "RESET" ? { kind: "idle" } : state;
}
}
export default function DataPanel() {
const [state, dispatch] = useReducer(reducer, { kind: "idle" } as Status);
return (
<div>
{state.kind === "idle" && <button onClick={() => dispatch({ type: "FETCH" })}>Cargar</button>}
{state.kind === "loading" && <p>Cargando...</p>}
{state.kind === "success" && <p>Recibido: {state.data}</p>}
{state.kind === "error" && <p>Error: {state.message}</p>}
</div>
);
}- Una unión discriminada hace imposibles las combinaciones inválidas (
loading: true, error: "...",data: "x", isLoading: true). - El reducer es el único lugar donde viven las transiciones -- trivial de testear unitariamente y de dibujar como un diagrama.
- Para máquinas de estado más grandes (estados jerárquicos, guards, efectos secundarios), gradúa a XState; la versión pequeña aquí cubre la mayoría de UIs.
- Empareja con
useTransitionouseActionStatepara disparar cambios de estado desde trabajo asincrónico.
Relacionado: Máquinas de estados para lógica de UI -- ejemplos más profundos, integración XState | useReducer -- el hook detrás de esto
11. Rendimiento: React.memo + Props estables
Previene que un child re-renderice cuando su padre re-renderiza pero sus props no han cambiado.
import { memo, useCallback, useState } from "react";
const Row = memo(function Row({
label, onSelect,
}: {
label: string;
onSelect: (label: string) => void;
}) {
return <li onClick={() => onSelect(label)}>{label}</li>;
});
export default function List({ items }: { items: string[] }) {
const [selected, setSelected] = useState<string | null>(null);
// Identidad estable -- useCallback evita que las filas memo'd re-renderizen
const onSelect = useCallback((label: string) => setSelected(label), []);
return (
<>
<p>Seleccionado: {selected ?? "ninguno"}</p>
<ul>
{items.map((item) => (
<Row key={item} label={item} onSelect={onSelect} />
))}
</ul>
</>
);
}memo(Component)salta re-renderizados cuando los props son referencialmente iguales al renderizado anterior.- Una función flecha inline crearía una nueva referencia en cada renderizado --
useCallbackle da una identidad estable para quememopueda realmente cortocircuitar. - Mide primero: el Profiler de React DevTools te dice qué componentes son costosos antes de que recurras a
memo. - Con el Compilador de React (React 19+), a menudo puedes eliminar completamente
memo/useCallback/useMemo-- el compilador lo maneja.
Relacionado: Optimización de rendimiento de React -- profiling, keys, virtualización de listas | Re-renderizados -- qué dispara renderizados y cómo reducirlos | Compilador de React -- memoización automática