Here’s a couple of lessons I’ve learned about how not to build React components. These are things I’ve come across over the past couple of months and thought they might be of interest to you if you’re working on a design system, especially one with a bunch of legacy technical decisions and a lot of tech debt under the hood.
Lesson 1: Avoid child components as much as you can
One thing about working on a big design system with lots of components is that the following pattern eventually starts to become problematic real quick:
<Card>
<Card.Header>Title</Card.Header>
<Card.Body><p>This is some content</p></Card.Body>
</Card>
The problematic parts are those child components, Card.Body
and Card.Header
. This example isn’t terrible because things are relatively simple — it’s when components get more complex that things can get bonkers. For example, each child component can have a whole series of complex props that interfere with the others.
One of my biggest pain points is with our Form components. Take this:
<Form>
<Input />
<Form.Actions>
<Button>Submit</Button>
<Button>Cancel</Button>
</Form.Actions>
</Form>
I’m simplifying things considerably, of course, but every time an engineer wants to place two buttons next to each other, they’d import Form.Actions
, even if there wasn’t a Form
on the page. This meant that everything inside the Form
component gets imported and that’s ultimately bad for performance. It just so happens to be bad system design implementation as well.
This also makes things extra difficult when documenting components because now you’ll have to ensure that each of these child components are documented too.
So instead of making Form.Actions
a child component, we should’ve made it a brand new component, simply: FormActions
(or perhaps something with a better name like ButtonGroup
). That way, we don’t have to import Form
all the time and we can keep layout-based components separate from the others.
I’ve learned my lesson. From here on out I’ll be avoiding child components altogether where I can.
Lesson 2: Make sure your props don’t conflict with one another
Mandy Michael wrote a great piece about how props can bump into one another and cause all sorts of confusing conflicts, like this TypeScript example:
interface Props {
hideMedia?: boolean
mediaIsEdgeToEdge?: boolean
mediaFullHeight?: boolean
videoInline?: boolean
}
Mandy writes:
The purpose of these props are to change the way the image or video is rendered within the card or if the media is rendered at all. The problem with defining them separately is that you end up with a number of flags which toggle component features, many of which are mutually exclusive. For example, you can’t have an image that fills the margins if it’s also hidden.
This was definitely a problem for a lot of the components we inherited in my team’s design systems. There were a bunch of components where boolean props would make a component behave in all sorts of odd and unexpected ways. We even had all sorts of bugs pop up in our Card
component during development because the engineers wouldn’t know which props to turn on and turn off for any given effect!
Mandy offers the following solution:
type MediaMode = 'hidden'| 'edgeToEdge' | 'fullHeight'
interface Props {
mediaMode: 'hidden'| 'edgeToEdge' | 'fullHeight'
}
In short: if we combine all of these nascent options together then we have a much cleaner API that’s easily extendable and is less likely to cause confusion in the future.
That’s it! I just wanted to make a quick note about those two lessons. Here’s my question for you: What have you learned when it comes to making components or working on design systems?
Love this post! We actually have made child components (we call them subcomponents) work rather well for our system, but we avoid some of the issues you’re talking about by having a rule about when we use them — subcomponents can only be rendered as a child of the parent, root component. This tends to align with usage of React context pretty well.
An example is our SegmentedControl component (https://sproutsocial.com/seeds/components/segmented-control), which renders a set of options in the form of a
SegmentedControl.Item
subcomponent. The parent component tracks the selected value of the control, and the items read that value off of the parent context to determine their active state. The API ends up looking like this:which feels pretty good to me. Following that rule of never using subcomponents outside of the parent component also helps us avoid the “importing a full component just for a subcomponent” problem.
One thing I’ve learnt is to keep the component API simple – i.e. the number of props being passed.
Taking a Button component as an example…
You could have a single Button component whose props determine if it’s a text-only button, or if it has an icon too (with all the additional icon positioning variants), or if it is only an icon button. Plus you’ll need all the standard props for size, colour, click handler, disabled state, etc. Lots of props.
Or have separate, simpler components… e.g. TextButton, TextIconButton, IconButton.
These can then share/extend types for size, colour, handler, disabled.
I’d love to separate text only buttons from icon-and-text buttons but in my case there are times where I need to only show an icon on mobile and text and icon on larger devices… Or an icon floated to the left on mobile and the same icon but on its own block and pushing down the text on portrait and up. So if I separate the components I’d have to tell whoever will work on them to hide/show one of the other depending on the breakpoint. What I do instead is have an icon prop that accepts either a config object to set all that up or just the icon itself in case they don’t need any of that.
Enrique, surely you can use CSS (media queries) to easily hide text on mobile, and use flexbox with flex-direction to change the icon position if required.
And if you don’t want to use CSS, detect mobile (via screen width) in React and conditionally render icon in JSX if not mobile size, etc.
That is actually what I’m doing under the hood, for the most part. Either just setting/removing atomic classes that either hide or show some element under certain screen sizes, or removing them from the DOM conditionally. The users of my component library just have to decide at what size to hide/show, move up/down, etc on a predefined set of breakpoints.
These frameworks are still just a MESS to work with. What’s the point if we need to limit the number of child components? It means they are poorly designed, period.
Wow, I really liked this article! I honestly never knew that having child elements could foul up how your code runs. I’m kind of new at coding, I don’t have a lot experience with big complex projects. I really hadden’t considered that my coding might be on the sloppy side and could lead to problems down the road. Thank you for this really useful tip.