Conceptos básicos de React Patterns
11 ejemplos para iniciarte con React Patterns -- 7 básicos y 4 intermedios.
Busca en todas las páginas de la documentación
11 ejemplos para iniciarte con React Patterns -- 7 básicos y 4 intermedios.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
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>
);
}children es lo que pones entre las etiquetas de apertura y cierre del componente.React.ReactNode acepta strings, números, elementos, fragmentos o null -- el tipo "renderizable" más amplio.<Card header={...} footer={...} />.Relacionado: Composición sobre herencia -- slots, componentes compuestos, patrones de layout | Componentes (Fundamentos) -- la prop
childrenen profundidad
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)} />;
}value/onChange (controlado) o defaultValue (no controlado), nunca los mezcles.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
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>
)}
/>
);
}render, children o 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
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>Tabs.Tab, Tabs.Panel) para una API limpia.useContext devuelve null, para que el mal uso falle ruidosamente.Relacionado: Componentes compuestos -- profundización, tipado TypeScript | Patrones de Context -- mejores prácticas 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>
);
}setTheme no re-renderizada cuando theme cambia.useMemo o useCallback para que el value del context de escritura permanezca referencialmente estable.Relacionado: Patrones de Context -- división, optimización, componentes del servidor | useContext -- el hook subyacente | Context vs. Zustand -- cuándo usar cada uno
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>getDerivedStateFromError aún no tiene equivalente con hooks.react-error-boundary (API más ligera y amigable con hooks) e inicia sesión en Sentry/Datadog en componentDidCatch.Relacionado: Error Boundaries -- react-error-boundary, Next.js
error.tsx, logging | Suspense -- el contraparte de carga
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) monta node dentro de container, pero los eventos aún burbujean hacia el padre React.typeof document === "undefined" para que sea seguro en SSR; de lo contrario, document lanza en el servidor.Relacionado: React Portals -- trampa de foco, accesibilidad, recetas de z-index | Componente Modal -- patrones de modal en producción
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 con use).<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
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");<P extends object>) para que el componente envuelto mantenga sus tipos de prop originales.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"
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>
);
}loading: true, error: "...", data: "x", isLoading: true).useTransition o useActionState para 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
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.useCallback le da una identidad estable para que memo pueda realmente cortocircuitar.memo.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
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥