Componentes
Construye interfaces de usuario reutilizables con componentes de función, el único tipo de componente que necesitas en React moderno.
Busca en todas las páginas de la documentación
Construye interfaces de usuario reutilizables con componentes de función, el único tipo de componente que necesitas en React moderno.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida, lista para copiar y pegar.
// Componente básico con props tipadas
interface GreetingProps {
name: string;
className?: string;
}
function Greeting({ name, className }: GreetingProps) {
return <h1 className={className}>Hola, {name}</h1>;
}
// Componente con children
function Card({ children, title }: { children: React.ReactNode; title: string }) {
return (
<div className="rounded border p-4">
<h2>{title}</h2>
{children}
</div>
);
}
// Props por defecto mediante desestructuración
function Button({ variant = "primary", ...rest }: ButtonProps) {
return <button className={variant} {...rest} />;
}Cuándo recurrir a esto: Siempre que tengas una pieza de UI que aparece más de una vez, o cuando una sección de JSX sea lo bastante compleja como para merecer su propio nombre.
"use client";
import { useState } from "react";
// --- Primitivas reutilizables ---
interface BadgeProps {
label: string;
color?: "green" | "yellow" | "red";
}
function Badge({ label, color = "green" }: BadgeProps) {
const colors = {
green: "bg-green-100 text-green-800",
yellow: "bg-yellow-100 text-yellow-800",
red: "bg-red-100 text-red-800",
};
return (
<span className={`inline-block rounded px-2 py-0.5 text-xs font-medium ${colors[color]}`}>
{label}
</span>
);
}
// --- Componente compuesto ---
interface Task {
id: number;
title: string;
status: "done" | "in-progress" | "blocked";
}
const statusColor: Record<Task["status"], BadgeProps["color"]> = {
done: "green",
"in-progress": "yellow",
blocked: "red",
};
function TaskItem({ task }: { task: Task }) {
return (
<li className="flex items-center justify-between border-b py-2">
<span>{task.title}</span>
<Badge label={task.status} color={statusColor[task.status]} />
</li>
);
}
// --- Componente contenedor ---
export function TaskBoard() {
const [tasks] = useState<Task[]>([
{ id: 1, title: "Set up CI pipeline", status: "done" },
{ id: 2, title: "Write integration tests", status: "in-progress" },
{ id: 3, title: "Deploy to production", status: "blocked" },
]);
return (
<section className="max-w-md rounded border p-4">
<h2 className="mb-3 text-lg font-bold">Tablero del sprint</h2>
<ul>
{tasks.map(task => (
<TaskItem key={task.id} task={task} />
))}
</ul>
</section>
);
}Qué demuestra esto:
Badge) compuestos en otros más grandes (TaskItem)TaskBoard) que es dueño del estado, componentes de presentación que lo reciben vía propsprops y devuelve React.ReactNode| Parámetro | Tipo | Descripción |
|---|---|---|
props | object | Todos los atributos pasados al componente, incluido children |
props.children | React.ReactNode | JSX anidado que se pasa entre las etiquetas de apertura y cierre |
| Retorno | React.ReactNode | JSX, null, string, number, boolean, o un arreglo de estos |
Patrón de slots (children con nombre):
interface LayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
}
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div className="grid grid-cols-[250px_1fr] grid-rows-[auto_1fr]">
<header className="col-span-2">{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}Render prop (extraer lógica):
interface ToggleProps {
children: (isOn: boolean, toggle: () => void) => React.ReactNode;
}
function Toggle({ children }: ToggleProps) {
const [isOn, setIsOn] = useState(false);
return <>{children(isOn, () => setIsOn(prev => !prev))}</>;
}
// Uso
<Toggle>
{(isOn, toggle) => (
<button onClick={toggle}>{isOn ? "ON" : "OFF"}</button>
)}
</Toggle>Componentes compuestos:
function Tabs({ children }: { children: React.ReactNode }) {
const [activeIndex, setActiveIndex] = useState(0);
return <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>{children}</TabsContext.Provider>;
}
Tabs.Panel = function Panel({ index, children }: { index: number; children: React.ReactNode }) {
const { activeIndex } = useContext(TabsContext);
return activeIndex === index ? <div>{children}</div> : null;
};// Tipar un componente que envuelve elementos nativos
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
variant?: "primary" | "secondary";
}
function Button({ variant = "primary", className, ...rest }: ButtonProps) {
return <button className={`btn-${variant} ${className ?? ""}`} {...rest} />;
}
// Atajo con React.PropsWithChildren
type CardProps = React.PropsWithChildren<{ title: string }>;Definir componentes dentro de componentes - Declarar function Inner() {} dentro de function Outer() {} crea una nueva identidad de componente en cada render, destruyendo el estado de Inner cada vez. Solución: Mueve Inner fuera de Outer, o memoízalo con useMemo si realmente necesitas cerrar sobre el ámbito del padre.
Mutar las props - props.items.push(newItem) muta los datos del padre, lo que provoca errores sutiles. Solución: Trata las props como inmutables. Crea nuevos arreglos/objetos y eleva los cambios de estado al propietario.
Falta de key al mapear - React advierte y puede perder el estado cuando los elementos carecen de claves. Solución: Proporciona una key estable y única a partir de tus datos (no el índice del arreglo salvo que la lista sea estática).
Abstraer demasiado pronto - Crear <FlexRow>, <Spacer> y <Text> para cada elemento añade indirección sin recompensa en reutilización. Solución: Espera a usar un patrón tres o más veces antes de extraer un componente.
| Alternativa | Úsala cuando | No la uses cuando |
|---|---|---|
| Server Components (RSC) | El componente solo lee datos y renderiza HTML estático, sin hooks ni manejadores de eventos | Necesitas useState, useEffect o APIs del navegador |
| Web Components | Necesitas elementos personalizados agnósticos al framework compartidos entre React, Vue, etc. | Estás completamente dentro de una base de código React |
| Parciales de plantilla (Handlebars) | Páginas renderizadas en el servidor sin un framework de JS | Necesitas interactividad del lado del cliente |
De una aplicación SaaS en producción con Next.js 15 / React 19 (SystemsArchitect.io).
// Ejemplo de producción: BackButton con props tipadas y valores por defecto
// Archivo: src/components/services/back-button.tsx
'use client';
import { useRouter } from 'next/navigation';
import { ArrowLeft } from 'lucide-react';
import { useLoadingStore } from '@/stores/loading-store';
interface BackButtonProps {
href?: string;
label?: string;
}
export default function BackButton({
href,
label = 'Volver a las secciones',
}: BackButtonProps) {
const router = useRouter();
const startLoading = useLoadingStore((state) => state.startLoading);
const handleClick = () => {
startLoading();
if (href) {
router.push(href);
} else {
router.back();
}
};
return (
<button onClick={handleClick} className="flex items-center gap-2 text-sm font-medium cursor-pointer transition-colors">
<ArrowLeft className="h-4 w-4" />
{label}
</button>
);
}Qué demuestra esto en producción:
? permiten un uso flexible. La prop href es opcional para que el botón pueda navegar a una ruta específica o retroceder en el historial.label = 'Volver a las secciones') ofrecen alternativas sensatas sin requerir una declaración defaultProps aparte.(state) => state.startLoading se suscribe únicamente a la acción startLoading, evitando re-renders cuando cambia otro estado del store de carga.interface sobre type para las props porque las interfaces producen mejores mensajes de error y admiten la fusión de declaraciones si es necesario.Un componente es una función que devuelve React.ReactNode (JSX, null, string, etc.) y es llamada por React durante el renderizado. Las funciones normales son llamadas por tu código. Los componentes deben empezar con una letra mayúscula para que JSX los trate como componentes, no como elementos HTML.
Prefiere interface para las props de los componentes. Las interfaces producen mejores mensajes de error de TypeScript y admiten la fusión de declaraciones. Usa type solo cuando necesites tipos unión o tipos mapeados.
Usa valores por defecto en la desestructuración dentro de la firma de la función:
function Button({ variant = "primary" }: ButtonProps) {
return <button className={variant}>Clic</button>;
}children es una prop especial que contiene el JSX que se anida entre las etiquetas de apertura y cierre de un componente. Tipéala como React.ReactNode. Úsala en componentes contenedores/de layout que no conocen su contenido de antemano.
Técnicamente sí, pero no lo hagas. React crea una nueva identidad de componente en cada render, destruyendo todo el estado del componente interno. Define siempre los componentes a nivel de módulo.
En lugar de una única prop children, acepta varias props con nombre tipadas como React.ReactNode:
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div>
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}Usa React.ComponentPropsWithoutRef<"element">:
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
variant?: "primary" | "secondary";
}useState o useReducer)Los Server Components se ejecutan únicamente en el servidor, pueden usar await sobre los datos directamente, tienen cero JavaScript del lado del cliente y no pueden usar hooks ni manejadores de eventos. Los componentes normales (de cliente) se ejecutan en el navegador y admiten interactividad.
Un tipo utilitario que añade children?: React.ReactNode a tu tipo de props:
type CardProps = React.PropsWithChildren<{ title: string }>;Úsalo cuando quieras una forma abreviada en lugar de tipar children manualmente.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥