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: dependency isolation bad good 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:

  1. We turned off the preflight mode to disable the default styling coming from Tailwind CSS.

  2. 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:

generic domain components example

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.