Compose, Don't Configure
There's a moment in every reusable component's life when something breaks. It started as a simple <Button> with primary and secondary variants. Six months later it has 23 boolean props, three renderX props, and nobody wants to open it because they know there's a five-level nested if inside.
People call it the prop-ocalypse, and it's the natural consequence of skipping composition pattern.
In this post:
- What composition pattern actually means (beyond the cliché)
- Why "configurable" components turn into hell
- How to apply it step by step with a real example (a
Card) - How to handle shared state between pieces
- How to wire it up with data fetching (the part where it really shines)
- When NOT to use it (yes, there are moments)
Let's go.
The problem: the component that knows too much
Let's start with a classic. You need to build a Card component for your design system. The first version is innocent:
type CardProps = {title: string;description: string;imageUrl?: string;};export const Card = ({ title, description, imageUrl }: CardProps) => (<div className="card">{imageUrl && <img src={imageUrl} alt={title} />}<h3>{title}</h3><p>{description}</p></div>);
Done. Until requirements pile up: action buttons, badges, two CTAs, prices, ratings, author info, timestamps. Six months in, the type looks like this:
type CardProps = {title: string;description: string;imageUrl?: string;imagePosition?: "top" | "left" | "right";actionLabel?: string;onAction?: () => void;secondaryActionLabel?: string;onSecondaryAction?: () => void;badge?: { text: string; variant: "success" | "warning" | "error" };price?: number;rating?: number;author?: { name: string; avatar: string };timestamp?: Date;variant?: "default" | "product" | "feed" | "dashboard";showFooter?: boolean;// ... 15 more props};
And inside, the component becomes a graveyard of ifs:
{variant === "product" && price && <PriceTag value={price} />;}{variant === "feed" && author && <AuthorBlock {...author} />;}{badge && <Badge variant={badge.variant}>{badge.text}</Badge>;}{showFooter && (actionLabel || secondaryActionLabel) && (<div className="card-footer">{/* ... */}</div>);}
This is a god component. And the problem isn't that it's "badly written." The problem is that the design pattern was wrong from the start.
What is composition pattern, for real?
Composition pattern is building complex components by combining smaller, independent pieces, instead of having one monolithic component you configure with props.
The idea is ripped straight from HTML. Think about <select>:
<select><optgroup label="Fruits"><option value="apple">Apple</option><option value="banana">Banana</option></optgroup></select>
Nobody ever wrote this:
<selectoptions='[{"type":"group","label":"Fruits","options":[{"value":"apple","label":"Apple"}]}]'/>
Because it would be horrible. HTML gives you pieces (select, optgroup, option) and you combine them however you need.
This is the core idea behind Fernando Rojo's talk "Composition Is All You Need" (Head of Mobile at Vercel, React Universe Conf 2025). If you haven't watched it yet, stop everything and go watch it. One of the best technical talks of the year.
Refactor: the Card as a compound component
Back to our Card. Instead of a component with 30 props, we're going to expose pieces the consumer composes:
import { ReactNode, createContext, useContext } from "react";// 1. Root piece: just the visual containerconst CardRoot = ({ children }: { children: ReactNode }) => (<div className="card">{children}</div>);// 2. Each part is independent and optionalconst CardImage = ({ src, alt }: { src: string; alt: string }) => (<img className="card__image" src={src} alt={alt} />);const CardHeader = ({ children }: { children: ReactNode }) => (<header className="card__header">{children}</header>);const CardTitle = ({ children }: { children: ReactNode }) => (<h3 className="card__title">{children}</h3>);const CardDescription = ({ children }: { children: ReactNode }) => (<p className="card__description">{children}</p>);const CardBadge = ({variant = "default",children,}: {variant?: "success" | "warning" | "error" | "default";children: ReactNode;}) => <span className={`card__badge card__badge--${variant}`}>{children}</span>;const CardFooter = ({ children }: { children: ReactNode }) => (<footer className="card__footer">{children}</footer>);// 3. Export everything under the same namespaceexport const Card = Object.assign(CardRoot, {Image: CardImage,Header: CardHeader,Title: CardTitle,Description: CardDescription,Badge: CardBadge,Footer: CardFooter,});
Now the consumer decides which pieces to use and in what order:
// Simple card<Card><Card.Title>Hello world</Card.Title><Card.Description>A basic card.</Card.Description></Card>// Product card<Card><Card.Image src="/sneaker.jpg" alt="Running shoe" /><Card.Badge variant="success">In stock</Card.Badge><Card.Header><Card.Title>Pro Running Shoe</Card.Title><span className="card__price">$120</span></Card.Header><Card.Description>Built for long distances.</Card.Description><Card.Footer><Button>Add to cart</Button></Card.Footer></Card>// Social feed card with author<Card><Card.Header><UserAvatar user={post.author} /><Card.Title>{post.author.name}</Card.Title><Timestamp date={post.createdAt} /></Card.Header><Card.Description>{post.content}</Card.Description><Card.Footer><LikeButton /><CommentButton /></Card.Footer></Card>
Look at what we gained:
- Zero internal
ifs inCard - Each piece has a single responsibility
- The consumer decides the structure without fighting props
- Adding a new variant doesn't require touching
Card - The call site reads like HTML, not like configuration
The real deal: composition pattern with data fetching
Up to now the example is nice but kind of academic. The case where composition pattern actually saves your life is when you throw data fetching into the mix.
Imagine you need to show a Post in different parts of the app: in the feed, on the detail page, in a search view. The layout changes everywhere, but the post data is the same.
The naive approach
We'll use TanStack Query for the fetching layer:
// hooks/usePostQuery.tsexport const usePostQuery = (postId: string) => {return useQuery({queryKey: ["post", postId],queryFn: () => fetchPost(postId),});};// PostCard.tsxexport const PostCard = ({ postId }: { postId: string }) => {const { data: post, isLoading, error } = usePostQuery(postId);if (isLoading) return <Skeleton />;if (error) return <ErrorState />;if (!post) return null;return (<article className="post-card"><header><img src={post.author.avatar} alt={post.author.name} /><h3>{post.author.name}</h3></header><h2>{post.title}</h2><p>{post.content}</p><footer><button>👍 {post.likes}</button><button>💬 {post.comments}</button></footer></article>);};
Works. But when the next request lands ("in the feed I only want title and author, no body") you start stacking props:
<PostCard postId="123" hideContent hideFooter compactHeader />
And we're back to the prop-ocalypse, but now with queries inside.
Composition pattern + fetching
The idea: a provider that fetches once, and pieces that consume that data without knowing how it was obtained.
// post-card/post-card-context.tsximport { createContext, useContext, ReactNode } from "react";import { usePostQuery } from "@/hooks/usePostQuery";type Post = {id: string;title: string;content: string;likes: number;comments: number;author: { name: string; avatar: string };createdAt: string;};const PostContext = createContext<Post | null>(null);export const usePost = (): Post => {const post = useContext(PostContext);if (!post) {throw new Error("PostCard.* must be used inside <PostCard>");}return post;};// post-card/post-card.tsxtype PostCardProps = {postId: string;children: ReactNode;fallback?: ReactNode;errorFallback?: ReactNode;};const PostCardRoot = ({postId,children,fallback = <PostCardSkeleton />,errorFallback = <PostCardError />,}: PostCardProps) => {const { data: post, isLoading, error } = usePostQuery(postId);if (isLoading) return <>{fallback}</>;if (error || !post) return <>{errorFallback}</>;return (<PostContext.Provider value={post}><article className="post-card">{children}</article></PostContext.Provider>);};
Now the pieces are small, focused, and know nothing about fetching:
// post-card/pieces.tsxconst PostAuthor = () => {const post = usePost();return (<div className="post-card__author"><img src={post.author.avatar} alt={post.author.name} /><span>{post.author.name}</span></div>);};const PostTitle = () => {const post = usePost();return <h2 className="post-card__title">{post.title}</h2>;};const PostContent = () => {const post = usePost();return <p className="post-card__content">{post.content}</p>;};const PostStats = () => {const post = usePost();return (<div className="post-card__stats"><button>👍 {post.likes}</button><button>💬 {post.comments}</button></div>);};const PostTimestamp = () => {const post = usePost();return (<time dateTime={post.createdAt}>{new Date(post.createdAt).toLocaleDateString()}</time>);};export const PostCard = Object.assign(PostCardRoot, {Author: PostAuthor,Title: PostTitle,Content: PostContent,Stats: PostStats,Timestamp: PostTimestamp,});
And here comes the magic
The same PostCard gets used in a thousand places, each composing what it needs:
// Feed: full card<PostCard postId={post.id}><PostCard.Author /><PostCard.Timestamp /><PostCard.Title /><PostCard.Content /><PostCard.Stats /></PostCard>// Related posts sidebar: just title and author<PostCard postId={post.id}><PostCard.Title /><PostCard.Author /></PostCard>// Search result: title and preview, no stats<PostCard postId={post.id}><PostCard.Title /><PostCard.Content /><PostCard.Timestamp /></PostCard>// Hover preview: title only<PostCard postId={post.id}><PostCard.Title /></PostCard>
Look at what we gained:
- One query per card, no matter how many pieces you use (TanStack Query also caches across cards if
postIdrepeats). - Loading and error are handled in one place. Pieces never see
undefined. - Zero optional chaining in pieces:
post.titlealways exists. - Adding a new layout doesn't require touching the component, you just compose differently.
- Each piece is testable in isolation by mocking the
PostContext.Providerwith a fake value.
And the coolest part: if tomorrow the API changes and usePostQuery now uses GraphQL, tRPC, or whatever, the pieces don't care. That's the decoupling you get from composition pattern done right.
Anti-patterns vs Composition: the dark side
❌ Anti-pattern 1: Stacking boolean props
<ModalshowHeadershowCloseButtonshowFooterhasBackdropisClosableisFullscreenshouldFocusOnOpen/>
Every new bool is a new code branch in the component. After 5 bools you have 32 possible combinations, and only 4 are actually tested.
❌ Anti-pattern 2: Render props for everything
<CardrenderHeader={() => <CustomHeader />}renderBody={() => <CustomBody />}renderFooter={() => <CustomFooter />}/>
Not terrible, but you're stuck halfway. If you're going to let the consumer render everything, give them pieces instead of slots.
❌ Anti-pattern 3: POJO-driven configuration
<DataTablecolumns={[{ id: "name", label: "Name", sortable: true, render: (v) => <b>{v}</b> },{ id: "age", label: "Age", align: "right" },]}/>
You're building a mini-DSL inside an object. Hard to type properly, hard to extend, and worse for the LLMs that'll help you write the code (yes, that matters now).
Better:
<DataTable data={users}><DataTable.Column id="name" sortable>{(user) => <b>{user.name}</b>}</DataTable.Column><DataTable.Column id="age" align="right">{(user) => user.age}</DataTable.Column></DataTable>
When NOT to use composition pattern
It's not a silver bullet. There are cases where a component with props is better:
- Hyper-specific, single-use components. A
LoginFormonly used at/login. Don't invent pieces for it. - When you need to enforce structure. If your design system says "every error has icon + title + description, no exceptions", an
<ErrorMessage>with props is safer than letting people compose freely. - Very simple components. A
<Spinner size="md" />doesn't need to be composed.
Mental rule: composition shines when you expect variability. If you know the component won't change much, plain props is simpler.
Rules for composing well
After many refactors, these are the principles I apply:
- One piece, one responsibility.
Card.Titleonly renders a title. Don't shove tracking logic in there. - Bring in Context only when you need it. If pieces don't share state, skip the Provider. YAGNI.
- Explicit throws in the Context hook. If someone uses
Card.Toggleoutside<Card>, fail loudly in dev, not silently in runtime. - Don't mix patterns. Don't have a
Cardthat accepts both<Card.Title>and atitleprop. Pick one.
Wrap-up
Composition pattern isn't just another trick for your CV. It's a different way of thinking about component APIs: instead of imagining every possible configuration and exposing them as props, you give the consumer the pieces and trust them to assemble their use case.
That requires a mental shift: you go from "components that control everything" to "components that offer capabilities." And once that click happens, everything else falls into place: your design system scales, your team has fewer arguments about new props, and components age well.
"Composition is all you need." — Fernando Rojo
That's the whole secret: you don't optimize for today, you optimize for the next six months.
Componé, no configures
En la vida de todo componente reutilizable llega un momento en que algo se rompe. Arrancó como un <Button> simple, con variantes primary y secondary. Seis meses después tiene 23 props booleanas, tres props renderX, y nadie lo quiere abrir porque todos saben que adentro hay un if anidado en cinco niveles.
A esto le dicen el prop-ocalypse, y es la consecuencia natural de saltearse el composition pattern.
En este post:
- Qué significa de verdad el composition pattern (más allá del cliché)
- Por qué los componentes "configurables" terminan siendo un infierno
- Cómo aplicarlo paso a paso con un ejemplo real (una
Card) - Cómo manejar estado compartido entre las piezas
- Cómo combinarlo con data fetching (la parte donde realmente brilla)
- Cuándo NO usarlo (sí, hay momentos)
Vamos.
El problema: el componente que sabe demasiado
Arranquemos con un clásico. Tenés que armar un componente Card para tu design system. La primera versión es inocente:
type CardProps = {title: string;description: string;imageUrl?: string;};export const Card = ({ title, description, imageUrl }: CardProps) => (<div className="card">{imageUrl && <img src={imageUrl} alt={title} />}<h3>{title}</h3><p>{description}</p></div>);
Listo. Hasta que se empiezan a acumular requerimientos: botones de acción, badges, dos CTAs, precios, ratings, info del autor, timestamps. A los seis meses, el tipo queda así:
type CardProps = {title: string;description: string;imageUrl?: string;imagePosition?: "top" | "left" | "right";actionLabel?: string;onAction?: () => void;secondaryActionLabel?: string;onSecondaryAction?: () => void;badge?: { text: string; variant: "success" | "warning" | "error" };price?: number;rating?: number;author?: { name: string; avatar: string };timestamp?: Date;variant?: "default" | "product" | "feed" | "dashboard";showFooter?: boolean;// ... 15 more props};
Y adentro, el componente se convierte en un cementerio de ifs:
{variant === "product" && price && <PriceTag value={price} />;}{variant === "feed" && author && <AuthorBlock {...author} />;}{badge && <Badge variant={badge.variant}>{badge.text}</Badge>;}{showFooter && (actionLabel || secondaryActionLabel) && (<div className="card-footer">{/* ... */}</div>);}
Esto es un god component. Y el problema no es que esté "mal escrito". El problema es que el patrón de diseño estuvo mal desde el principio.
¿Qué es el composition pattern, posta?
El composition pattern es armar componentes complejos combinando piezas más chicas e independientes, en lugar de tener un único componente monolítico que configurás con props.
La idea está sacada directo de HTML. Pensá en <select>:
<select><optgroup label="Fruits"><option value="apple">Apple</option><option value="banana">Banana</option></optgroup></select>
Nadie escribió nunca esto:
<selectoptions='[{"type":"group","label":"Fruits","options":[{"value":"apple","label":"Apple"}]}]'/>
Porque sería horrible. HTML te da piezas (select, optgroup, option) y vos las combinás como necesites.
Esta es la idea central de la charla de Fernando Rojo "Composition Is All You Need" (Head of Mobile en Vercel, React Universe Conf 2025). Si todavía no la viste, frená todo y andá a verla. Una de las mejores charlas técnicas del año.
Refactor: la Card como compound component
Volvamos a nuestra Card. En vez de un componente con 30 props, vamos a exponer piezas que el consumidor compone:
import { ReactNode, createContext, useContext } from "react";// 1. Root piece: just the visual containerconst CardRoot = ({ children }: { children: ReactNode }) => (<div className="card">{children}</div>);// 2. Each part is independent and optionalconst CardImage = ({ src, alt }: { src: string; alt: string }) => (<img className="card__image" src={src} alt={alt} />);const CardHeader = ({ children }: { children: ReactNode }) => (<header className="card__header">{children}</header>);const CardTitle = ({ children }: { children: ReactNode }) => (<h3 className="card__title">{children}</h3>);const CardDescription = ({ children }: { children: ReactNode }) => (<p className="card__description">{children}</p>);const CardBadge = ({variant = "default",children,}: {variant?: "success" | "warning" | "error" | "default";children: ReactNode;}) => <span className={`card__badge card__badge--${variant}`}>{children}</span>;const CardFooter = ({ children }: { children: ReactNode }) => (<footer className="card__footer">{children}</footer>);// 3. Export everything under the same namespaceexport const Card = Object.assign(CardRoot, {Image: CardImage,Header: CardHeader,Title: CardTitle,Description: CardDescription,Badge: CardBadge,Footer: CardFooter,});
Ahora el consumidor decide qué piezas usar y en qué orden:
// Simple card<Card><Card.Title>Hello world</Card.Title><Card.Description>A basic card.</Card.Description></Card>// Product card<Card><Card.Image src="/sneaker.jpg" alt="Running shoe" /><Card.Badge variant="success">In stock</Card.Badge><Card.Header><Card.Title>Pro Running Shoe</Card.Title><span className="card__price">$120</span></Card.Header><Card.Description>Built for long distances.</Card.Description><Card.Footer><Button>Add to cart</Button></Card.Footer></Card>// Social feed card with author<Card><Card.Header><UserAvatar user={post.author} /><Card.Title>{post.author.name}</Card.Title><Timestamp date={post.createdAt} /></Card.Header><Card.Description>{post.content}</Card.Description><Card.Footer><LikeButton /><CommentButton /></Card.Footer></Card>
Mirá lo que ganamos:
- Cero
ifs internos enCard - Cada pieza tiene una sola responsabilidad
- El consumidor decide la estructura sin pelearse con props
- Sumar una variante nueva no requiere tocar
Card - El código donde se usa se lee como HTML, no como configuración
Lo que importa de verdad: composition pattern con data fetching
Hasta acá el ejemplo está lindo pero es medio académico. El caso donde el composition pattern realmente te salva la vida es cuando metés data fetching en la ecuación.
Imaginate que tenés que mostrar un Post en distintas partes de la app: en el feed, en la página de detalle, en una vista de búsqueda. El layout cambia en cada lugar, pero los datos del post son los mismos.
El enfoque ingenuo
Vamos a usar TanStack Query para la capa de fetching:
// hooks/usePostQuery.tsexport const usePostQuery = (postId: string) => {return useQuery({queryKey: ["post", postId],queryFn: () => fetchPost(postId),});};// PostCard.tsxexport const PostCard = ({ postId }: { postId: string }) => {const { data: post, isLoading, error } = usePostQuery(postId);if (isLoading) return <Skeleton />;if (error) return <ErrorState />;if (!post) return null;return (<article className="post-card"><header><img src={post.author.avatar} alt={post.author.name} /><h3>{post.author.name}</h3></header><h2>{post.title}</h2><p>{post.content}</p><footer><button>👍 {post.likes}</button><button>💬 {post.comments}</button></footer></article>);};
Funciona. Pero cuando cae el próximo pedido ("en el feed quiero solo título y autor, sin el cuerpo") empezás a apilar props:
<PostCard postId="123" hideContent hideFooter compactHeader />
Y volvimos al prop-ocalypse, pero ahora con queries adentro.
Composition pattern + fetching
La idea: un provider que hace el fetch una sola vez, y piezas que consumen esos datos sin saber cómo se obtuvieron.
// post-card/post-card-context.tsximport { createContext, useContext, ReactNode } from "react";import { usePostQuery } from "@/hooks/usePostQuery";type Post = {id: string;title: string;content: string;likes: number;comments: number;author: { name: string; avatar: string };createdAt: string;};const PostContext = createContext<Post | null>(null);export const usePost = (): Post => {const post = useContext(PostContext);if (!post) {throw new Error("PostCard.* must be used inside <PostCard>");}return post;};// post-card/post-card.tsxtype PostCardProps = {postId: string;children: ReactNode;fallback?: ReactNode;errorFallback?: ReactNode;};const PostCardRoot = ({postId,children,fallback = <PostCardSkeleton />,errorFallback = <PostCardError />,}: PostCardProps) => {const { data: post, isLoading, error } = usePostQuery(postId);if (isLoading) return <>{fallback}</>;if (error || !post) return <>{errorFallback}</>;return (<PostContext.Provider value={post}><article className="post-card">{children}</article></PostContext.Provider>);};
Ahora las piezas son chicas, enfocadas y no saben nada del fetching:
// post-card/pieces.tsxconst PostAuthor = () => {const post = usePost();return (<div className="post-card__author"><img src={post.author.avatar} alt={post.author.name} /><span>{post.author.name}</span></div>);};const PostTitle = () => {const post = usePost();return <h2 className="post-card__title">{post.title}</h2>;};const PostContent = () => {const post = usePost();return <p className="post-card__content">{post.content}</p>;};const PostStats = () => {const post = usePost();return (<div className="post-card__stats"><button>👍 {post.likes}</button><button>💬 {post.comments}</button></div>);};const PostTimestamp = () => {const post = usePost();return (<time dateTime={post.createdAt}>{new Date(post.createdAt).toLocaleDateString()}</time>);};export const PostCard = Object.assign(PostCardRoot, {Author: PostAuthor,Title: PostTitle,Content: PostContent,Stats: PostStats,Timestamp: PostTimestamp,});
Y acá viene la magia
El mismo PostCard se usa en mil lugares, y cada uno compone lo que necesita:
// Feed: full card<PostCard postId={post.id}><PostCard.Author /><PostCard.Timestamp /><PostCard.Title /><PostCard.Content /><PostCard.Stats /></PostCard>// Related posts sidebar: just title and author<PostCard postId={post.id}><PostCard.Title /><PostCard.Author /></PostCard>// Search result: title and preview, no stats<PostCard postId={post.id}><PostCard.Title /><PostCard.Content /><PostCard.Timestamp /></PostCard>// Hover preview: title only<PostCard postId={post.id}><PostCard.Title /></PostCard>
Mirá lo que ganamos:
- Una query por card, no importa cuántas piezas uses (y TanStack Query además cachea entre cards si el
postIdse repite). - Loading y error se manejan en un solo lugar. Las piezas nunca ven un
undefined. - Cero optional chaining en las piezas:
post.titlesiempre existe. - Sumar un layout nuevo no requiere tocar el componente, simplemente componés distinto.
- Cada pieza se puede testear aislada mockeando el
PostContext.Providercon un valor falso.
Y lo mejor: si mañana cambia la API y usePostQuery pasa a usar GraphQL, tRPC o lo que sea, a las piezas no les importa. Ese es el desacople que te da un composition pattern bien hecho.
Anti-patterns vs. composición: el lado oscuro
❌ Anti-pattern 1: apilar props booleanas
<ModalshowHeadershowCloseButtonshowFooterhasBackdropisClosableisFullscreenshouldFocusOnOpen/>
Cada bool nuevo es una rama de código nueva en el componente. Con 5 bools tenés 32 combinaciones posibles, y solo 4 están testeadas de verdad.
❌ Anti-pattern 2: render props para todo
<CardrenderHeader={() => <CustomHeader />}renderBody={() => <CustomBody />}renderFooter={() => <CustomFooter />}/>
No es terrible, pero te quedaste a mitad de camino. Si vas a dejar que el consumidor renderice todo, dale piezas en vez de slots.
❌ Anti-pattern 3: configuración a base de POJOs
<DataTablecolumns={[{ id: "name", label: "Name", sortable: true, render: (v) => <b>{v}</b> },{ id: "age", label: "Age", align: "right" },]}/>
Estás armando un mini-DSL adentro de un objeto. Difícil de tipar bien, difícil de extender, y peor para los LLMs que te van a ayudar a escribir el código (sí, eso ahora importa).
Mejor así:
<DataTable data={users}><DataTable.Column id="name" sortable>{(user) => <b>{user.name}</b>}</DataTable.Column><DataTable.Column id="age" align="right">{(user) => user.age}</DataTable.Column></DataTable>
Cuándo NO usar el composition pattern
No es una bala de plata. Hay casos donde un componente con props es mejor:
- Componentes hiperespecíficos, de un solo uso. Un
LoginFormque solo se usa en/login. No le inventes piezas. - Cuando necesitás imponer una estructura. Si tu design system dice "todo error lleva ícono + título + descripción, sin excepciones", un
<ErrorMessage>con props es más seguro que dejar que cada uno componga libremente. - Componentes muy simples. Un
<Spinner size="md" />no necesita composición.
Regla mental: la composición brilla cuando esperás variabilidad. Si sabés que el componente no va a cambiar mucho, props comunes y corrientes es más simple.
Reglas para componer bien
Después de muchos refactors, estos son los principios que aplico:
- Una pieza, una responsabilidad.
Card.Titlesolo renderiza un título. No le metas lógica de tracking. - Sumá Context solo cuando lo necesitás. Si las piezas no comparten estado, salteate el Provider. YAGNI.
- Throws explícitos en el hook del Context. Si alguien usa
Card.Togglefuera de<Card>, que falle fuerte en dev, no en silencio en runtime. - No mezcles patrones. No tengas una
Cardque acepte tanto<Card.Title>como una proptitle. Elegí uno.
Cierre
El composition pattern no es un truco más para el CV. Es otra forma de pensar las APIs de los componentes: en lugar de imaginarte todas las configuraciones posibles y exponerlas como props, le das las piezas al consumidor y confiás en que arme su caso de uso.
Eso pide un cambio de cabeza: pasás de "componentes que controlan todo" a "componentes que ofrecen capacidades". Y una vez que hace click, todo lo demás se acomoda solo: tu design system escala, tu equipo discute menos sobre props nuevas y los componentes envejecen bien.
"Composition is all you need." — Fernando Rojo
Ese es todo el secreto: no optimizás para hoy, optimizás para los próximos seis meses.