Componentes
Construye interfaces de usuario reutilizables con componentes de función, el único tipo de componente que necesitas en React moderno.
Receta
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.
Ejemplo práctico
"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:
- Componentes pequeños y enfocados (
Badge) compuestos en otros más grandes (TaskItem) - Interfaces de TypeScript que hacen cumplir los contratos de props
- Valores de props por defecto mediante desestructuración
- Componente contenedor (
TaskBoard) que es dueño del estado, componentes de presentación que lo reciben vía props
Análisis a fondo
Cómo funciona
- Un componente de función es cualquier función que acepta un único objeto
propsy devuelveReact.ReactNode - React llama a tu función en cada render: el cuerpo de la función se ejecuta de arriba a abajo cada vez
- Las props son de solo lectura: un componente nunca muta sus propias props
- Los componentes se vuelven a renderizar cuando su padre se vuelve a renderizar o cuando cambia su propio estado o contexto
- React usa la posición del componente en el árbol (no su nombre) para preservar el estado entre renders
Parámetros y valores de retorno
| 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 |
Patrones de composición
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;
};Notas sobre TypeScript
// 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 }>;Errores comunes
-
Definir componentes dentro de componentes — Declarar
function Inner() {}dentro defunction Outer() {}crea una nueva identidad de componente en cada render, destruyendo el estado de Inner cada vez. Solución: MueveInnerfuera deOuter, o memoízalo conuseMemosi 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
keyal mapear — React advierte y puede perder el estado cuando los elementos carecen de claves. Solución: Proporciona unakeyestable 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.
Alternativas
| 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 |
Ejemplo del mundo real
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:
- Las props opcionales con
?permiten un uso flexible. La prophrefes opcional para que el botón pueda navegar a una ruta específica o retroceder en el historial. - Los valores por defecto en la desestructuración (
label = 'Volver a las secciones') ofrecen alternativas sensatas sin requerir una declaracióndefaultPropsaparte. - El selector de Zustand
(state) => state.startLoadingse suscribe únicamente a la acciónstartLoading, evitando re-renders cuando cambia otro estado del store de carga. - Se prefiere
interfacesobretypepara las props porque las interfaces producen mejores mensajes de error y admiten la fusión de declaraciones si es necesario. - El componente es una unidad de responsabilidad única: gestiona la navegación con un indicador de carga, nada más.
Preguntas frecuentes
¿Cuál es la diferencia entre un componente y una función normal?
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.
¿Debería usar interface o type para las props?
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.
¿Cómo establezco valores por defecto para las props?
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>;
}¿Qué es la prop children y cuándo debería usarla?
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.
¿Puedo definir un componente dentro de otro componente?
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.
¿Qué es el patrón de slots para componentes?
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>
);
}¿Cuándo debería extraer un nuevo componente?
- Cuando una pieza de JSX se usa en varios lugares
- Cuando una sección de un componente tiene su propio estado o lógica
- Cuando un archivo de componente supera las ~150 líneas y tiene límites claros
- No extraigas prematuramente: espera a usar un patrón al menos tres veces
¿Cómo extiendo las props de un elemento HTML nativo en mi componente?
Usa React.ComponentPropsWithoutRef<"element">:
interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
variant?: "primary" | "secondary";
}¿Qué provoca que un componente se vuelva a renderizar?
- Su padre se vuelve a renderizar
- Su propio estado cambia (vía
useStateouseReducer) - Un contexto que consume cambia
- El mero cambio de las props no dispara un re-render: lo que lo hace es que el padre se vuelva a renderizar
¿Qué es un componente contenedor frente a un componente de presentación?
- Los componentes contenedores son dueños del estado, obtienen datos y manejan la lógica
- Los componentes de presentación reciben datos vía props y se centran en renderizar la UI
- Esta separación mantiene los componentes enfocados y testeables, aunque es una guía, no una regla estricta
¿En qué se diferencian los React Server Components de los componentes de función normales?
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.
¿Qué es React.PropsWithChildren y cuándo lo uso?
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.
Relacionado
- JSX y TSX — la sintaxis que devuelven los componentes
- Renderizado condicional — elegir qué muestra un componente
- Eventos — hacer que los componentes sean interactivos
- useRef / Reenvío de refs — exponer manejadores del DOM desde los componentes