In the previous article, we went through the different architectural strategies we considered for migrating from Bootstrap to Tailwind CSS, and then I shared the one we chose.
In this article, I will go through the actual execution of the picked strategy and share some anti-patterns and learned lessons throughout the whole experience.
A quick recap of the chosen strategy
Following the previous article, you now know that we chose Strategy 4, where we integrated Tailwind CSS and used it on top of Bootstrap. This way, we could continue delivering features to the end users while updating and styling certain pages step by step with Tailwind CSS.
Using two libraries for styling is an anti-pattern, but it was the most suitable decision, both from a business and technical perspective. For more info about the pros and cons of the strategy, see here.
The tech stack
We extended the old technical stack with the following:
The execution phase
Now, let’s dive into the most exciting part of the series → the execution of our architectural decision (Strategy 4).
Before starting the implementation, we revisited how we used react-bootstrap
in the project. We discovered we were referencing it throughout the whole codebase. And this was another anti-pattern we had in our project.
That’s how we came up with Step 1.
Step 1: Isolate react-bootstrap
By isolating react-bootstrap
in our codebase, we could later replace one component from the package with our implementation. We could do that without updating the files where we used the component.
Consider the following example:
When isolating the Button
component, we could replace its implementation in ../components/ui/Button.js
with our custom one. We would update only one file. Later, we could substitute react-bootstrap
without introducing significant changes in the project.
Yet, we had to update all files that referenced import { Button } from 'react-bootstrap';
. That was very risky, and it brought an enormous potential for breaking something.
That's why we moved all react-bootstrap
's components into one place.
💡 Isolate dependencies.
Step 2: Integrate Tailwind CSS
We followed the tailwindcss installation guide to install and configure it in our project.
But, we did two crucial changes in our configuration:
-
We turned off the
preflight
mode to disable the default styling coming from Tailwind CSS. -
We added a custom
prefix
to all Tailwind CSS classes, so as not to have any collisions with the Bootstrap ones.
This enabled us to use classes both from Bootstrap and Tailwind CSS at the same time without interference. For example:
<span className="mt-2 ms-px-2 ms-text-amber-300 ms-border">Hello World!</span>
Step 3: Rewrite components when possible
In Steps 1 and 2, we isolated react-bootstrap
components into a single place, and we configured Tailwind CSS. Next, we sought opportunities to extract reusable React components styled with Tailwind CSS.
Since we would be creating new React components, we decided to split them into two categories:
-
Generic Components styled with Tailwind CSS. Our goal was to prepare for creating a Reusable React Component UI Library, which we could use throughout our client's projects in the future.
-
Domain Components tailored to our Domain and used throughout the pages in the current web application.
To better illustrate the idea, consider the image bellow:
In this example, we can have a generic Button
component and a LoadingButton
as a Domain Button component.
💡 A key principle we followed was Atomic Design, where we’re not designing pages; we’re designing systems of components.
Also, we didn’t change the components’ APIs. We followed the ones from react-bootstrap
because we didn’t want to introduce many changes at once, only style updates.
One key aspect of this step was using twin.macro
to have tailwindcss
classes in our styled-components.
This is how we rewrote react-bootstrap
's Badge
component to use Tailwind CSS and keep the same API.
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import styled from '@emotion/styled/macro';
import tw from 'twin.macro';
const StyledBadge = styled('span')`
${tw`ms-px-2 ms-py-1 ms-inline-flex ... ...`}
&.primary {
${tw`ms-bg-primary-100 ms-text-primary-500`}
}
...
&.pill {
${tw`ms-rounded-full`}
}
&.xs {
${tw`ms-text-xs`}
}
...
`;
const Badge = forwardRef(
({ className, as, variant, size, pill, ...props }, ref) => (
<StyledBadge
{...props}
ref={ref}
className={cx('ms-badge', className, variant, size, {
pill,
})}
as={as}
/>
),
);
Badge.propTypes = {
className: PropTypes.string,
pill: PropTypes.bool,
variant: PropTypes.string,
size: PropTypes.string,
as: PropTypes.elementType,
};
Badge.defaultProps = {
className: '',
pill: false,
variant: 'primary',
size: undefined,
as: 'span',
};
export default Badge;
In the end, our Folder Structure looked like this:
/src
│ ...
| ...
│ AppRoutes.js
│
└───components --> reusable Domain Components used throughout the Pages
│ │ Header.js
│ │ ...
│ └───ui --> Generic Components (preparing for UI lib)
│ │ index.js
│ │ Button.js
| | Badge.js
│ │ ...
└───pages --> actual Pages on our web app
│ │ ...
│ └───login
│ │ index.js
│ │ ...
└───...
│
└───...
Conclusion
I hope you enjoyed the Migrating from Bootstrap to Tailwind CSS blog post series. We explored different architectural strategies for migrating from one UI library to another. Along the way, we learned something new, like the importance of conducting good research before implementing а specific architectural decision, and isolating dependencies. We also discovered how to use Tailwind CSS in our styled components. The experience taught us valuable lessons in achieving maintainability, good software architecture, and project structure, and avoiding potential anti-patterns.
There are a few pieces of practical advice I'd like to point out:
-
Strive for incremental improvements. Rome was not built in a day. Neither will be your “perfect” application, so having some “bad code” is okay.
-
Be careful about anti-patterns and code smells. Try to avoid them in advance.
💡 Sometimes you can accept having an anti-pattern in your codebase but you should be very aware of why you have it and at what cost.
If you liked my blog posts, you can follow me on LinkedIn and Twitter, where I share coding tips daily. Hope to see you there.