Here’s a Card component in React:
const Card = props => {
return(
<div className="card">
<h2>{props.title}</h2>
<p>{props.content}</p>
</div>
)
}
It might be pretty useful! If you end up using this thing hundreds of times, now you have the ability to refactor a little bit of HTML across your app very easily. You already have that power in CSS because of the class name there, but now you have HTML control too. Feel it.
But wait. Maybe this is limiting… an <h2>
? What if that really should have been an <h4>
in some usages? What’s the approach there? Maybe an API of sorts?
const Card = props => {
return(
<div className="card">
{props.type === "big" && <h2>{props.title}</h2>}
{props.type !== "big" && <h4>{props.title}</h4>}
<p>{props.content}</p>
</div>
)
}
Or maybe we force a level to be passed in?
const Card = props => {
const HeaderTag = `h${props.level}`;
return(
<div className="card">
<HeaderTag>{props.title}</HeaderTag>
<p>{props.content}</p>
</div>
)
}
Or maybe that header is its own component?
And a forced paragraph tag wrapper around that content? That’s a little limiting, isn’t it? Maybe that should be a <div>
so that it could take arbitrary HTML inside it, like multiple paragraphs.
const Card = props => {
return(
<div className="card">
<WhateverHeader>{props.title}</WhateverHeader>
<div>{props.content}</div>
</div>
)
}
Actually, why even ask for content with props? It’s probably easier to deal with a child component, especially if what is coming over is HTML.
const Card = props => {
return(
<div className="card">
<WhateverHeader>{props.title}</WhateverHeader>
{children}
</div>
)
}
There are more assumptions we could challenge too. Like card only for a class name… shouldn’t that be more flexible?
const Card = props => {
const classes = `card ${props.className}`;
return(
<div className={classes}>
<WhateverHeader>{props.title}</WhateverHeader>
{children}
</div>
)
}
I’m still forcing card
there. We could drop that so that it isn’t assumed, or build another aspect of the Card API providing a way to opt-out of it.
Even the <div>
wrapper is presumptuous. Perhaps that tag name could be passed in so that you could make it into a <section>
or <article>
or whatever you want.
Maybe it’s better to assume nothing actually, making our card like this:
const Card = () => {
return(
<>
{children}
</>
)
}
That way anything you want to change, you have the freedom to change. At least then it’s flexibility while being relaxed about it, rather than this kind of “flexibility”:
<Card
parentTag="article"
headerLevel="3"
headerTitle="My Card"
contentWrapper="div"
cardVariation="extra-large"
contentContent=""
this=""
little=""
piggy=""
went=""
to=""
market=""
/>
That kind of extreme-API-zying just happens sometimes when you’re grasping for control and flexibility at the same time.
A component model with no guidance can lead to over-componentization also, like perhaps:
const Card = props => {
return(
<CardWrapperTheme>
<CardWrapper>
<CardTitle />
<CardContent />
<CardFooter />
</CardWrapper>
</CardWrapperTheme>
)
}
There might be perfectly good reasons to do that, or it might be the result of componentizing because it’s “free” and just feels like that’s how things are done in an architecture that supports it.
There is a balance. If a component is too strict, it runs the risk of that people won’t use them because they don’t give them what they need. And if they’re too loose, people might not use them because they don’t provide any value, and, even if they did use them, they don’t offer any cohesiveness.
I don’t have any answers here, I just find it fascinating.
I feel like the last way is much better. It gives you much better composition. You can do prop forwarding and ref forwarding. It’s been called “compound components” — in other words, React element is not meant to be stand-alone, instead they’re collectively assembled to make one thing – a card. For more complex components with internal state, you can use context to pass things around. For the wrapper, and not being able to decide if it should be a div or something else, the emerging pattern is an
as
prop used like this:There’s a lot more to consider but when you add it all up, this compound components type of strategy is really the best one for building stuff like this I feel. Not trying to make this a marketing thing, but this is what we do for Reach: https://gist.github.com/ryanflorence/e5c794e6093d16a69fa88d2112a292f7#one-to-one-rendered-dom-element-to-reach-component. Worth noting that the people who coined the term compound components are also the ones that make Reach (and I also work with them just for full transparency)
No, simplicity is always best. If I am adding one card component then I expect to only import one card component. I am not going to import cardheader, cardbody, cardwhatever when all its doing div outputting div.
If something is so different it needs a billion sub components then it’s a different component, period.
IT IS ! (fascinating).
Thanks for this great post.
The version just before « I’m still forcing card there » is the one that comes closest to the right compromise, I think.
Yes I would agree (though other solutions might be better depending on the situation). I ended up doing pretty much exactly that with twig templates when creating some nested server-rendered Gutenberg blocks for a project recently. It provides a nice level of flexibility with innerBlocks and the block className attribute that’s provided by default.
I’ve never used React so I cannot speak to the actual examples given… but I feel like this is something I deal with a lot with CSS.
When developing a UI element there is always that question of where the line is between “This is flexible enough to be reused in different areas” and “This is just a declaration of 27,000 [Insert Framework Here] classes”
Consider Bootstrap for example….
That’s their ‘Card’ component. You could however create something exceedingly similar using nothing but their helper / utility classes:
Perfectly functional, and probably a great way to learn how to make the most out of those utility classes. But it’s also so abstract that it stops being a component and starts being just a hodge-podge.
What about creating “AbstractCard” which has all the settings you can imagine and then create simple most used component(s) with little settings which uses AbstractCard internaly?
Then user can decide if he wants to use very simple or configurable component.