Solucionar la sobrecarga de propiedades en React con composición y slots
Aprenda por qué los props de React con mucha configuración generan deuda de mantenimiento, y cómo la inversión de control, la composición y los slots permiten crear componentes verdaderamente reutilizables.
Fuentes: Your React Components Aren’t Reusable. They’re Just Complicated
Un análisis de cómo las propiedades controladas por la configuración acumulan silenciosamente deudas de mantenimiento, y cómo la inversión de control junto con la composición conducen a componentes de interfaz realmente modulares.
Imagínese una solicitud de integración enviada por un compañero que presenta lo que denomina con orgullo "Tabla de Datos Universal."
El objetivo es satisfacer todas las necesidades de renderizado de tablas en todo el código base de una sola vez.
Viene con treinta y cuatro propiedades separadas:
<UniversalDataTable
data={users}
columns={columns}
enablePagination={true}
paginationType="cursor"
enableSorting={true}
showFilterRow={true}
hasExportToCsvButton={true}
exportButtonText="Download CSV"
headerBackground="light-gray"
denseRows={false}
enableRowSelection={true}
selectionMode="multiple"
renderCustomRowActions={(row) => <DropdownMenu row={row} />}
onRowClick={(row) => navigate(`/users/${row.id}`)}
/>
Dentro de ella hay aproximadamente 1,200 líneas de lógica.
Para renderizar una lista de usuarios, funciona muy bien.
Los requisitos son: tres columnas, íconos de marca de verificación junto a cada fila de características y un contorno resaltado en "Popular" alrededor de la columna central.
Alguien intenta usar <UniversalDataTable />, esperando que funcione sin problemas.
Es entonces cuando todo se complica.
No existe forma de personalizar los bordes de las celdas individuales.
La alineación de los encabezados está fijada en un diseño flex específico.
Para obtener un resultado limpio, hay que pasar hasExportToCsvButton={false} junto con varios otros parámetros de sobrescritura.
Por eso el equipo añade cuatro propiedades más:
isPricingTable
popularColumnIndex
customHeaderRenderer
disableGlobalStyles
El archivo crece hasta llegar a 1,450 líneas.
En este punto ya no es reutilizable en absoluto.
Lo que comenzó como una abstracción se ha convertido en un enredo de casos especiales que fingen ser un único componente.
1. La trampa de las propiedades de configuración
Cuando un equipo desea que un componente sea “reutilizable”, el enfoque instintivo suele ser la configuración a través de propiedades.
Cada nuevo caso de uso activa otra bandera, enumeración de cadenas, función de callback o sobrescritura de estilo que se añade a la interfaz de props.
Un ejemplo típico:
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
hasLeftIcon?: boolean;
leftIcon?: React.ReactNode;
hasRightIcon?: boolean;
rightIcon?: React.ReactNode;
isFullWidth?: boolean;
tooltipText?: string;
tooltipPosition?: 'top' | 'bottom';
rounded?: boolean;
}
A primera vista parece adaptable.
El problema es que la flexibilidad basada en configuraciones conlleva un costo que no se percibe de inmediato.
1. Explosión combinatoria
Solo diez props booleanos ya generan, en teoría, 1,024 combinaciones posibles.
No es necesario probarlas todas para darse cuenta de lo rápido que esto se escapa a control.
Ciertas combinaciones son contradictorias entre sí.
Otras simplemente permanecerán sin usarse para siempre.
Algunas se renderizarán de manera inconsistente según el ancho de la ventana, el contenido circundante o cuán profundamente esté anidado el componente.
Con el tiempo, lo que se obtiene es un montón de casos límite en lugar de una API coherente y predecible.
2. Exploración profunda de props
Digamos que una configuración de tooltip debe llegar a un contenedor anidado oculto en varios niveles dentro del árbol de componentes.
Cada componente intermedio en el camino ahora tiene que reenviar una propiedad para la cual no tiene uso real.
La configuración se transmite a través de los niveles simplemente porque la estructura lo exige, no porque tenga sentido allí.
Ese es el mecanismo detrás del proceso de propagación de propiedades.
3. Suposiciones rígidas sobre el diseño
Aquí es donde los componentes basados en configuración suelen fallar definitivamente.
Supongamos que un nuevo diseño requiere que se coloque un ícono entre dos líneas de texto de botón.
Agregar algo como:
iconPosition="between"
puede solucionar el problema por ahora.
Pero, ¿qué pasa en cuanto el diseño cambie de nuevo?
En poco tiempo tendrás que mantener todo un conjunto de variantes:
iconPosition="left"
iconPosition="right"
iconPosition="top"
iconPosition="between"
iconPosition="absolute"
La lista sigue expandiéndose porque el JSX interno del componente sigue dictando por completo el diseño.
El verdadero problema no es cuántos props existen.
Sino que el componente ha asumido demasiada responsabilidad por cosas que están fuera de su ámbito de competencia.
2. Inversión de Control: Un modelo mental mejor
La verdadera reutilizabilidad no se logra intentando predecir con antelación cada requisito futuro.
Surge al aplicar la Inversión de Control (IoC).
El concepto es sencillo:
En lugar de que el componente dicte todo lo que se renderiza dentro de él, deja que quien utilice el componente controle las partes que necesitan cambiar.
Esta es precisamente la razón por la cual la composición es un patrón tan importante en React.
En lugar de crear un botón que tenga integrada la funcionalidad para tooltips, íconos, spinners, insignias y todas las posibles combinaciones de diseño, construya un botón cuya función principal sea simplemente ser un botón.
Por ejemplo:
export function Button({
children,
variant = 'primary',
className = '',
...props
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} ${className}`}
{...props}
>
{children}
</button>
);
}
El control sobre el contenido ahora está en manos del usuario:
<Tooltip content="Save your profile">
<Button>
<SaveIcon />
<span>Save Changes</span>
</Button>
</Tooltip>
Fíjese en lo que ya no es necesario.
El botón no tiene conocimiento alguno de la existencia de tooltips.
No existe la propiedad hasLeftIcon.
Tampoco existe la propiedad leftIcon.
No existe la propiedad iconSpacing.
Tampoco existe la propiedad hasSpinner.
Necesita un spinner de carga? Simplemente colóquelo directamente dentro del botón.
<Button>
<Spinner />
Saving...
</Button>
El componente reduce su tamaño al tiempo que gana flexibilidad.
Ese equilibrio es la idea clave aquí.
Un componente reutilizable no necesita manejar internamente cada escenario posible. Necesita un límite bien definido contra el cual otros componentes puedan componerse.
3. Los slots facilitan la extensión de componentes complejos
Componer con children funciona bien para elementos simples. Pero, ¿qué ocurre con componentes más elaborados como tarjetas, diálogos, paneles de control o paneles de perfil?
Es aquí donde los slots y los componentes compuestos resultan útiles.
Imagínese una tarjeta de perfil convencional construida a partir de una serie de propiedades:
<UserProfileCard
avatarUrl="/avatar.jpg"
name="Sarah Connor"
role="Tech Lead"
status="Active"
bio="Building reliable frontend systems."
primaryActionLabel="Send Message"
onPrimaryAction={sendMessage}
secondaryActionLabel="View Profile"
onSecondaryAction={viewProfile}
badgeColor="green"
/>
A primera vista, esto parece conveniente.
Pero luego los requisitos comienzan a cambiar.
El equipo de marketing quiere que el handle social de la persona aparezca justo al lado de su nombre. El equipo administrativo de las cuentas empresariales necesita un banner que diga “Suspended” extendido a lo ancho de la tarjeta. Otro equipo de producto desea tres botones de acción en lugar de dos. Un diseñador decide que el avatar debe colocarse en el lado derecho y no en el izquierdo.
Cada uno de estos pedidos se convierte en otro elemento adicional.
Un enfoque mejor es exponer el diseño como piezas componibles:
<Card>
<Card.Header>
<Avatar
src={user.avatarUrl}
alt={user.name}
/>
<div>
<Card.Title>{user.name}</Card.Title>
<Card.Subtitle>{user.role}</Card.Subtitle>
</div> <Badge variant={user.statusColor}>
{user.status}
</Badge>
</Card.Header> <Card.Body>
<p>{user.bio}</p>
</Card.Body> <Card.Actions>
<Button
variant="secondary"
onClick={viewProfile}
>
View Profile
</Button> <Button
variant="primary"
onClick={sendMessage}
>
Send Message
</Button>
</Card.Actions>
</Card>
Ahora puedes ver realmente la estructura de la tarjeta en el JSX. Y como puedes verla, puedes modificarla.
¿Quieres un botón de acción más? Agrégalo. ¿Necesitas un distintivo diferente? Cámbialo. ¿Tienes que añadir otro banner? Insértalo donde corresponda. ¿Quieres un arreglo de encabezado completamente distinto? Simplemente escribe un nuevo encabezado.
Nada de esto requiere tocar un componente compartido sobredimensionado para soportar una sola variante nueva.
4. La composición supera a la configuración
Estos dos enfoques plantean preguntas fundamentalmente diferentes.
La configuración pregunta:
"¿Qué conjunto de opciones debería exponer este componente?"
La composición pregunta:
"¿Qué pequeños bloques de construcción pueden utilizar los usuarios para crear lo que realmente necesitan?"
La configuración sigue cargando más responsabilidades de toma de decisiones sobre el propio componente. La composición devuelve esas decisiones a quien utiliza el componente. Por lo general, ese es un lugar más adecuado donde residir dicha responsabilidad.
Tome esta comparación como ejemplo:
<Button
hasLeftIcon
leftIcon={<SaveIcon />}
hasTooltip
tooltipText="Save your profile"
showSpinner={isSaving}
spinnerPosition="left"
/>
versus:
<Tooltip content="Save your profile">
<Button>
{isSaving && <Spinner />}
<SaveIcon />
Save Profile
</Button>
</Tooltip>
El segundo fragmento contiene más JSX, cierto. Pero también tiene mucho menos acoplamiento entre las partes que no necesitan saber nada unas de otras. Este intercambio suele valer la pena: un poco más de marcado en el lugar donde se hace la llamada tiende a ser más económico a largo plazo que añadir otra funcionalidad permanente a un componente compartido.
5. Aprende de las bibliotecas headless
Varias bibliotecas modernas para frontend demuestran esta filosofía de manera excepcional.
Proyectos como Radix UI, TanStack Table y shadcn/ui han desempeñado un papel importante en la popularización de este enfoque basado en la composición.
TanStack Table es uno de los ejemplos más claros. No te obliga a usar un diseño de tabla fijo. En lugar de eso, proporciona la lógica y la gestión de estado para aspectos como:
- Ordenamiento
- Filtrado
- Paginación
- Selección de filas
La tarea de convertir ese estado en marcado real depende exclusivamente de usted. Por ejemplo:
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{/* Your own rendering logic */}
</tbody>
</table>
La forma exacta de la API no es realmente lo importante aquí. Lo que importa es la arquitectura subyacente: el motor de tablas se encarga del comportamiento, mientras que usted se ocupa de la presentación. Dado que estas dos funciones están separadas, puede rediseñar los elementos visuales sin que la lógica central de las tablas se convierta en un componente visual enorme.
6. Separe el comportamiento de la apariencia
Esto conduce naturalmente a otra directriz que vale la pena seguir:
Mantenga separado lo que hace un componente de cómo se renderiza.
Un único componente puede necesitar lidiar con una cantidad considerable de complejidad interna, abarcando aspectos como:
- Navegación por teclado
- Gestión del foco
- Selección
- Ordenamiento
- Filtrado
- Estatuto
- Accesibilidad
- Posicionamiento
Gestionar todo eso adecuadamente rara vez es sencillo. Sin embargo, nada de esto obliga al mismo componente a determinar también exactamente cómo debe verse el resultado final.
Esa es la idea central detrás de la mayoría de los sistemas UI sin interfaz gráfica. El comportamiento se reutiliza en muchas implementaciones visuales, mientras que la capa de presentación permanece completamente abierta. Esa separación suele generar una reutilización mucho mayor que intentar crear un único componente enorme lleno de docenas de ajustes visuales.
7. Cuándo los props realmente tienen sentido
Nada de esto significa que los props sean un patrón deficiente.
Son una de las herramientas más fundamentales que React pone a tu disposición.
El problema no es que existan los props, sino usarlos para codificar cada variación estructural imaginable que un componente pueda necesitar.
Ejemplos como estos están completamente bien:
<Button variant="primary" />
<Button size="sm" />
<Input disabled />
<Card className="featured" />
La bandera roja aparece una vez que un componente comienza a acumular una lista como esta:
<Component
showHeader
showFooter
showBadge
showIcon
showActions
iconPosition="left"
badgePosition="right"
footerAlignment="center"
actionLayout="horizontal"
compact
dense
bordered
rounded
disableAnimation
customHeaderRenderer={...}
customFooterRenderer={...}
/>
Una vez que detectes ese patrón, vale la pena detenerse y preguntarse:
"¿Deberían seguir siendo props, o algunos de ellos funcionarían mejor como hijos compuestos?"
Solo hacer esa pregunta suele evitar mucha complejidad innecesaria en el futuro.
8. Qué te cuesta realmente la sobreabstracción
El verdadero daño causado por componentes sobreconfigurados no es estético.
Es que se vuelven costosos de modificar.
Un componente ampliamente utilizado tiene un gran radio de impacto. Si veinte lugares en tu código dependen de él, entonces cada nuevo prop que se añada puede afectar a todos esos veinte consumidores.
Los ingenieros dudan en tocarlo.
Escribir documentación para ello se vuelve más difícil.
Probarlo se vuelve más difícil.
Localizar errores en él se vuelve más difícil.
Con el tiempo, el componente se vuelve tan completamente “reutilizable” que nadie realmente quiere reutilizarlo.
Esa es la ironía inherente a este patrón.
La sobreabstracción puede socavar precisamente la reutilizabilidad que se pretendía lograr.
Una primitiva pequeña y de alcance limitado suele resultar más reutilizable en la práctica que un componente extenso creado para anticipar cada posible variante.
Un marco práctico para construir componentes
Antes de recurrir a otra propiedad de configuración, realice estas verificaciones.
1. ¿Estoy lidiando con el comportamiento o con la presentación?
Si se trata de un comportamiento, es probable que una propiedad o un gancho personalizado sea la herramienta adecuada. Si se trata únicamente de la estructura visual, la composición suele ser la opción más adecuada.
2. ¿Realmente necesita este componente esta información?
Si se espera que un único componente entienda tooltips, ganchos de análisis, insignias, iconos, indicadores de carga y toda disposición de sus hijos, casi con certeza ha asumido más responsabilidades de las que debería.
3. ¿Podría children manejar esto en su lugar?
Si es posible, prefiera la composición en lugar de introducir otra propiedad relacionada con el contenido.
4. ¿Ayudarían los espacios nombrados a aclarar la API?
En componentes más complejos, definir regiones explícitas como:
<Card.Header />
<Card.Body />
<Card.Footer />
puede hacer que sea mucho más fácil comprender su estructura.
5. ¿Se puede separar la lógica del marcado?
Si es así, merece la pena considerar un enfoque sin interfaz gráfica o basado primero en la lógica.
6. ¿Este componente resuelve una necesidad real o imaginaria?
Esta pregunta es más importante de lo que parece.
Evite agregar cosas como:
futureFeature={true}
solo porque alguien podría quererlo en el futuro.
Diseñe según requisitos para los cuales tenga pruebas reales, no aquellos sobre los que solo esté especulando.
Reflexiones finales
El objetivo de crear componentes React reutilizables no es producir algo que pueda hacer absolutamente todo.
Sino crear piezas que puedan ensamblarse de muchas maneras diferentes.
Esa distinción parece sutil, pero arquitectónicamente cambia todo.
Un componente que depende de treinta propiedades puede parecer flexible a primera vista, pero cada una de esas propiedades añade otra responsabilidad, otro estado que debe gestionarse y otra combinación que alguien tendrá que soportar eventualmente.
Por el contrario, un componente con una API limitada y límites de composición claros puede parecer sencillo.
No obstante, en la práctica suele mantenerse mucho mejor con el paso del tiempo.
Así que la próxima vez que sientas la tentación de añadir otra propiedad a un componente compartido, detente un momento y pregúntate:
“¿Estoy haciendo que sea más reutilizable, o solo más configurable?”
Estas dos cosas no son intercambiables.
Un componente no se vuelve reutilizable al acumular treinta opciones.
Se vuelve reutilizable al realizar bien una tarea, establecer límites razonables y no interferir en otros aspectos.
La arquitectura de componentes sólidos no se trata de prever cada necesidad futura.
Se trata de hacer que cualquier necesidad futura que surja sea fácil de combinar.
Lecturas relacionadas
- Construyendo una página de detalle de filmos resiliente con Next.js App Router — Aprenda cómo obtener y almacenar en caché los datos de la API OMDB correctamente en Next.js App Router utilizando componentes de servidor asíncronos, parámetros esperados y un manejo adecuado del error 404.
- Los éxitos silenciosos de TypeScript 6 y los hábitos de JavaScript de los desarrolladores senior — Descubra las características pasadas por alto de TypeScript 6, como la gestión explícita de recursos y los parámetros de tipo const, además de los modismos en JavaScript en los que confían diariamente los ingenieros senior.