Component-based frontend frameworks like React support the DRY principle: Don’t Repeat Yourself. Following the DRY principle is an excellent way to eliminate code duplication.
But unfortunately, sometimes, developers overdo it. And that leads to strange hard-to-debug behavior and code that is more complex than it needs to be. The result is code that I call Too DRY.
In this article, I want to demonstrate the issue on the example of positioning components with a layout system such as the Bootstrap Grid System.
Both junior and senior developers make this mistake, and the problem is not just limited to positioning components. I see the issue with code in general.
In this article, I will cover:
- What the DRY Principle is
- What the Single-responsibility Principle is
- What the Small Functions Principle is
- How to spot layout-related violations of the Single-responsibility Principle
- How to fix layout-related violations of the Single-responsibility Principle
- What the downsides of injecting style into child components are
Component-based frameworks
Using component-based frontend frameworks like React comes with a set of benefits. And some of these benefits support, directly or indirectly, important software engineering principles.
Divide & Conquer
Component-based means we can break complex user interfaces down into sub-components, where each sub-component solves a part of the bigger problem.
Dividing complex problems into smaller, easier-to-solve problems is actually a common theme in software engineering. It happens on a small scale – when we group statements into sub-routines (a.k.a. functions). And it happens on a large scale – when we divide a complex monolithic server application into single-page apps and microservices.
The driving software engineering principle is the first of the five SOLID principles: The Single-responsibility Principle.
A class or function should do one thing only – and it should do that one thing well.
Single-responsibility Principle
That means we should strive to build components that deal only with one aspect of the overall problem.
Maintainability
Software that is based on components is also cheaper to maintain. If our code-base is messy, it will take longer to figure out how to change or add things.
If we look at the top-level component of a React app, for instance, we can get an overview of what components the app is made of:
const App = () => (
<>
<Header />
<Body>
<Menu />
<Shop />
</Body>
<Footer />
</>
)
If we’re not interested in the details, we will not be overwhelmed with unnecessary information. However, if we are interested in the details of a specific sub-component, we can simply open the corresponding file and dig deeper.
The driving principle here is a Clean Code principle: Small Functions.
Functions should be small. How small? Very small!
Small Functions
A function with more than 7 statements is too big!
Reusability
Another advantage of using component-based frameworks is that we can save time and money by reusing existing components.
An important software engineering principle that drives reusability is the DRY principle:
DRY – Don’t Repeat Yourself
Let’s take the following JSX code as an example:
<h1>React Benefits</h1>
<div className='d-flex flex-column gap-4'>
<div className='card'>
<div className='card-header'>
Declarative
</div>
<div className='card-body'>
<div className='card-text'>
React makes it painless to create interactive...
</div>
</div>
</div>
<div className='card'>
<div className='card-header'>
Component-Based
</div>
<div className='card-body'>
<div className='card-text'>
Build encapsulated components that manage...
</div>
</div>
</div>
<div className='card'>
<div className='card-header'>
Learn Once, Write Anywhere
</div>
<div className='card-body'>
<div className='card-text'>
We don’t make assumptions about the rest...
</div>
</div>
</div>
</div>
The code declares three cards, each describing a Benefit of the React framework:
The code above violates the Don’t Repeat Yourself principle. The means, the markup of every card looks more or less the same. A card has a card-header
and a card-body
. A card-body
has card-text
. The only difference is the text itself.
Fortunately, with React we can create a sub-component and eliminate the code duplication:
const Benefit = ({ title, children: description }) => (
<div className='card'>
<div className='card-header'>
{title}
</div>
<div className='card-body'>
<div className='card-text'>
{description}
</div>
</div>
</div>
)
The new Benefit
component renders one card. It takes the title
as property and puts it into the card-header
. It also expects the description
to be passed as a child component.
Thanks to the new Benefit
component, we can now significantly reduce the amount of code in our parent component.
<h1>React Benefits</h1>
<div className='d-flex flex-column gap-4'>
<Benefit title='Declarative'>
React makes it painless to create interactive...
</Benefit>
<Benefit title='Component-Based'>
Build encapsulated components that manage...
</Benefit>
<Benefit title='Learn Once, Write Anywhere'>
We don’t make assumptions about the rest...
</Benefit>
</div>
The code becomes also more readable, which in turn makes it more maintainable.
And if we wanted to change the style of all cards, we have to touch only one location in the code.
Let’s change the background color of the card-header
to the theme’s info
color:
const Benefit = ({ title, children: description }) => (
<div className='card'>
<div className='card-header bg-info'>
{title}
</div>
<!-- ... -->
</div>
)
By changing only one line of code, we can change the appearance of all cards:
These are the primary reasons why you should not repeat yourself: to keep the code-base small and maintainable and to save time.
Don’t Repeat Yourself can lead to problems
Yes, DRY can help eliminate code duplication. That’s a good thing. But as with everything good, too much of it can be bad.
From time to time, I observe developers overdoing it with the DRY principle. Let me give you an example:
<h1>Students</h1>
<span className='position-relative alert alert-info p-2 mx-2'>
John
<span className='position-absolute top-100 start-100
translate-middle badge bg-primary'>
B-
</span>
</span>
<span className='position-relative alert alert-info p-2 mx-2'>
Jane
<span className='position-absolute top-100 start-100
translate-middle badge bg-primary'>
A+
</span>
</span>
<span className='position-relative alert alert-info p-2 mx-2'>
Jim
<span className='position-absolute top-100 start-100
translate-middle badge bg-primary'>
B+
</span>
</span>
The above markup uses Bootstrap classes to create a list of students with grades:
Each student has a grade displayed as a small badge in the lower right corner.
We can clearly see the code duplication. So let’s eliminate it using DRY.
First, let’s introduce a Badge
component:
const Badge = ({ children: title }) => (
<span className='position-absolute top-100 start-100
translate-middle badge bg-primary'>
{title}
</span>
)
Using it in the parent component improves the situation somewhat:
<h1>Students</h1>
<span className='position-relative alert alert-info p-2 mx-2'>
John
<Badge>B-</Badge>
</span>
<span className='position-relative alert alert-info p-2 mx-2'>
Jane
<Badge>A+</Badge>
</span>
<span className='position-relative alert alert-info p-2 mx-2'>
Jim
<Badge>B+</Badge>
</span>
Next, let’s create a Student
component:
const Student = ({ name, grade }) => (
<span className='position-relative alert alert-info p-2 mx-2'>
{name}
<Badge>{grade}</Badge>
</span>
)
This is how our parent component looks like now:
<h1>Students</h1>
<Student name='John' grade='B-' />
<Student name='Jane' grade='A+' />
<Student name='Jim' grade='B+' />
The rendered output has not changed a bit:
All is well.
Now imagine that some time has passed.
In a different part of the same app, we want to display a list of students with grade badges:
We look through the existing components and find a <Badge>
component. Great! Let’s reuse it in our new list:
<ul>
<li>John <Badge>B-</Badge></li>
<li>Jane <Badge>A+</Badge></li
<li>Jim <Badge>B+</Badge> </li>
</ul>
But the result is not what we expected:
Where are the badges?
It looks like all the badges ended up in the lower right corner of the screen.
What the fuck!
Understanding the problem
So what went wrong?
The problem we are facing is that earlier we have applied the DRY principle in a rather naive way. We extracted the Badge
component by cutting out the duplicate code and pasting it into its own component:
const Badge = ({ children: title }) => (
<span className='position-absolute top-100 start-100
translate-middle badge bg-primary'>
{title}
</span>
)
We satisfied the DRY principle, but at the same time, did we violate another principle of software engineering: The Single-responsibility Principle.
What are the responsibilities of the Badge
component?
- rendering a badge with a
primary
background - translating that badge into the lower right corner
A class or function should do one thing only – and it should do that one thing well.
Single-responsibility Principle
The Badge
component above does two things, and it actually does the second thing poorly. The translation is performed relative – relative to the next parent component that has position-relative
applied.
The Badge
component does only half of the job and fails to communicate that. The user of the Badge
component has to do something on top of just using it.
That works for the Student
component. It puts a position-relative
span around the Badge
:
const Student = ({ name, grade }) => (
<span className='position-relative alert alert-info p-2 mx-2'>
{name}
<Badge>{grade}</Badge>
</span>
)
But it does not work for our new list:
<ul>
<li>John <Badge>B-</Badge></li>
<li>Jane <Badge>A+</Badge></li>
<li>Jim <Badge>B+</Badge> </li>
</ul>
We used the Badge
, and we did not know that we have to wrap it in a parent with position-relative
. As a result, all three badges are positioned relative to the viewport.
Fixing the problem
We could try to fix the issue by wrapping each Badge
in a parent element with position-relative
. But in this case, we actually don’t want the badges transitioned into the lower right corner.
We want the badge to appear behind the student’s name:
The real problem is that the Badge
component violates the Single-responsibility Principle. It “knows” too much, but actually not enough to do a good job.
Here’s what we should do instead:
Let’s remove the aspect of translating the badge into the lower right corner from the Badge
component:
const Badge = ({ children: title }) => (
<span className='badge bg-primary'>
{title}
</span>
)
And let’s move it into the Student
component where it belongs:
const Student = ({ name, grade }) => (
<span className='position-relative alert alert-info p-2 mx-2'>
{name}
<span className='position-absolute top-100 start-100
translate-middle'>
<Badge>{grade}</Badge>
</span>
</span>
)
Now the new list of students looks as expected:
The Badge
component does not translate anything. Only in the Student
component are things translated. But our new list does not use the Student
component. The badges stay behind the students’ names – exactly where we want them to be.
<ul>
<li>John <Badge>B-</Badge></li>
<li>Jane <Badge>A+</Badge></li>
<li>Jim <Badge>B+</Badge> </li>
</ul>
Keep in mind that the described problem is just an example. It does not just apply to React or Bootstrap code. The mistake is also not limited to CSS classes only. You can make this mistake with inline styling as well. And it does not even have to be frontend-related code at all.
All over the world, can we observe junior and senior developers violating the Single-responsibility Principle in favor of DRY.
Do Repeat Yourself
Let’s have a look at a slightly modified version of our React Benefits. This time we are using a Grid Layout with rows and columns:
<h1>React Benefits</h1>
<div className='row'>
<div className='col-4'>
<div className='card'>
<div className='card-header'>
Declarative
</div>
<div className='card-body'>
<div className='card-text'>
React makes it painless to create interactive...
</div>
</div>
</div>
</div>
<div className='col-4'>
<div className='card'>
<div className='card-header'>
Component-Based
</div>
<div className='card-body'>
<div className='card-text'>
Build encapsulated components that manage...
</div>
</div>
</div>
</div>
<div className='col-4'>
<div className='card'>
<div className='card-header'>
Learn Once, Write Anywhere
</div>
<div className='card-body'>
<div className='card-text'>
We don’t make assumptions about the rest...
</div>
</div>
</div>
</div>
</div>
Applying the DRY principle naively (cut-and-paste) would result in the following Benefit
component:
const Benefit = ({ title, children: description }) => (
<div className='col-4'>
<div className='card'>
<div className='card-header'>
{title}
</div>
<div className='card-body'>
<div className='card-text'>
{description}
</div>
</div>
</div>
</div>
)
But now the layout is spread again over two components. The Benefit
component “knows” about columns and the parent component “knows” about rows:
<h1>React Benefits</h1>
<div className='row'>
<Benefit title='Declarative'>
React makes it painless to create interactive...
</Benefit>
<Benefit title='Component-Based'>
Build encapsulated components that manage...
</Benefit>
<Benefit title='Learn Once, Write Anywhere'>
We don’t make assumptions about the rest...
</Benefit>
</div>
Robert C. Martin, who coined the term Single-responsibility Prinicple, said:
A class [or function] should have only one reason to change.
Robert C. Martin
When we decide that the Benefit
presentation as card needs to be altered, the Benefit
component will need to be changed.
But when we decide to change the layout of the cards (e.g. 2 columns instead of 3), the Benefit
component will also need to be changed (col-6
instead of col-4
– assuming a 12-column grid system).
Two reasons for change. A clear violation of the Single-reponsibility Principle.
Let’s fix that by removing the column <div className='col-4'>
:
const Benefit = ({ title, children: description }) => (
<div className='card'>
<div className='card-header'>
{title}
</div>
<div className='card-body'>
<div className='card-text'>
{description}
</div>
</div>
</div>
)
And pulling it back up into the parent component:
<h1>React Benefits</h1>
<div className='row'>
<div className='col-4'>
<Benefit title='Declarative'>
React makes it painless to create interactive...
</Benefit>
</div>
<div className='col-4'>
<Benefit title='Component-Based'>
Build encapsulated components that manage...
</Benefit>
</div>
<div className='col-4'>
<Benefit title='Learn Once, Write Anywhere'>
We don’t make assumptions about the rest...
</Benefit>
</div>
</div>
Yes, in the parent component, there is some code duplication now. But in this case, having some code duplication is better than the alternative: more WTFs per minute.
A Benefit
component renders a card. That’s it. One responsibility.
The parent component positions the cards. That’s it. One responsibility.
If we wanted to change the layout from a 3-column layout to a 2-column layout now, we’d only have to change the parent component. The Benefit
component stays untouched.
Think twice before eliminating all code duplication. A Benefit
is about the clear presentation of information and not about the position of that information on the page.
If you choose the wrong abstraction, you might violate other software engineering principles, which in turn will make your code less maintainable and more expensive to work with.
Duplication is far cheaper than the wrong abstraction.”
Sandi Metz
How to spot the violation
Layout often works by applying some classes on parent level and some classes on child level.
System | Parent | Child |
---|---|---|
CSS Grid | display: grid | grid-column-end: 5 |
Bootstrap Grid | row | col-4 |
CSS Flexbox | display: flex | align-self: flex-end |
Bootstrap Flexbox | d-flex | align-self-end |
CSS Position | position: relative | position: absolute |
Bootstrap Position | position-relative | position-absolute |
Whenever we see a layout instruction that is usually associated with a child component, and there is no corresponding parent layout instruction to be seen around, chances are that we have a violation of the Single-responsibility Principle in front of us.
For instance, whenever we see a col
without a corresponding row
, someone has most likely chosen the wrong abstraction. row
and col
belong together. One does make little sense without the other.
Best Practices
Let children grow
We established now that, in most instances, we want to keep the layout aspect out of child components. The parent component should be in charge of controlling the layout.
If the parent component creates a 3-column layout, we usually expect the child components which form the columns’ content to play along.
Thanks to grid layout systems and flexbox, positioning child components horizontally and vertically should be relatively easy. If that’s not the case, check if your child component does something layout-related. It shouldn’t.
Adjusting the size of child components should also be easy. In a column layout, we expect the child components to use all available space in a column. If that’s not the case, check if your child component is responsive. Consider adding the equivalent of width: 100%
to your child component.
Bootstrap is an example of a popular library that actually applies this principle. Input elements, for instance, take all available space by default. And the size is controlled by either using Bootstrap’s Grid or Flex system.
Avoid injecting style
Some developers try to reduce the code duplication by passing the className
or inline style
into the child component as additional property:
<h1>React Benefits</h1>
<div className='row'>
<Benefit className='col-4' title='Declarative'>
React makes it painless to create interactive...
</Benefit>
<Benefit className='col-4' title='Component-Based'>
Build encapsulated components that manage...
</Benefit>
<Benefit className='col-4' title='Learn Once, Write Anywhere'>
We don’t make assumptions about the rest...
</Benefit>
</div>
This keeps knowledge about rows
and cols
together in the parent component, but it hardly reduces the code duplication.
Worse, the child component is now more complex, and it certainly violates the Single-responsibility Principle:
const Benefit = ({ className, title, children: description }) => (
<div className={className}>
<div className='card'>
<div className='card-header'>
{title}
</div>
<div className='card-body'>
<div className='card-text'>
{description}
</div>
</div>
</div>
</div>
)
What are the responsibilities of the Benefit
component?
- rendering a card with
title
anddescription
- wrapping the card in a
<div>
and apply the passedclassName
Chances are good that soon another developer wants to use the Benefit
component and inject some style using the style
attribute. The Benefit
component is touched again and becomes even more complex.
I don’t recommend injecting classNames
or style
in most instances. Injecting style always reminds me of a botched plastic surgery.
There is, however, one use case:
Library components
Components that come as part of a well-thought-through, well-maintained library sometimes support additional classNames
and style
attributes.
And that’s ok.
Library code, in general, is different from code written for our single-page apps or microservices.
Defensive programming, for instance, is a practice that is appropriate when it comes to implementing a function for a library. But for normal app development, I see defensive programming as a bad practice. It leads to overengineering and wasted time and money.
Library developers will also put a lot of effort into self-explanatory error messages, for instance, when a function is called with parameters of the wrong type. Library developers maintain written documentation. And when it comes to components, they often put in the effort to support custom styling – not just for the top-level element but also for nested elements.
Library developers do this because their function will be used in dozens of different scenarios by hundreds of other developers.
When you are building a regular app or microservice, your code will not be used in dozens of different scenarios by hundreds of other developers.
Supporting additional classNames
(injecting style), checking parameter types, and annotating parameters with JSDoc declarations often represent a special form of Premature Optimization. Don’t waste your time and your employer’s or client’s money by making your code more complex than it needs to be.
Layout components
If you’re really concerned about layout-related code duplication, consider creating layout components:
<h1>React Benefits</h1>
<Row>
<Col>
<Benefit title='Declarative'>
React makes it painless to create interactive...
</Benefit>
</Col>
<Col>
<Benefit title='Component-Based'>
Build encapsulated components that manage...
</Benefit>
</Col>
<Col>
<Benefit title='Learn Once, Write Anywhere'>
We don’t make assumptions about the rest...
</Benefit>
</Col>
</Row>
const Row = ({ children }) => <div className='row'>{children}</div>
const Col = ({ children }) => <div className='col-4'>{children}</div>
Here, the saved amount of code is marginal. But it might make sense when the layout is more verbose (e.g., when the <divs>
have many classes or elaborate style instructions).
I would leave Row
or Col
in the same file as helper functions. And only if there’s a good reason to reuse these components elsewhere in the app would I move them into their own files.
Summary
Component-based frameworks like React support important software engineering principles like DRY and Small Functions. But we have to balance these principles with other principles like the Single-responsibility Principle.
Don’t Repeat Yourself is not just about slicing code into pieces. Don’t rip aspects apart that should stay together. In terms of components, we might end up with components that behave in an unpredictable way when we try to reuse them. Or the components grow too complex.
Instead, let’s make sure that our components do one thing well.
In terms of layout, let’s create child components that use all the available space, and let’s implement parent components that are in complete control of all layout aspects.
Rather than choosing the wrong abstraction, it’s ok to accept some code duplication.
See you around,
-David