Don't Marry Your UI Library · The Sashka
technical

Don't Marry Your UI Library

Stop console.log debugging. Learn systematic approaches to finding and fixing bugs faster, with real-world examples from production systems.

Alex Raihelgaus
Don't Marry Your UI Library

Don’t Marry Your UI Library

When starting new projects, or rewriting existing ones, and we talk about what frontend tooling to use, I always strive to not be dependent on the tooling.

For example, in React, if your app is heavily using React hooks, and you decide to step away from React to any other framework (or god forbid simple HTML & CSS), it’ll be painful.

But if you build your project in a way where React is just a plugin, where you’re agnostic to ‘how React expects me to do things’, and you decide to rebuild your app using Svelte now, or any other tool, the task becomes way simpler. And with the help of LLMs, way way simpler.

Same trick with managing design systems - by using a facade that your app knows about and uses, you don’t care what UI kit you’re using. It becomes an implementation detail, something way smaller than using components directly all over the app.

The Problem: UI Library Lock-In

Imagine you’re using Material UI.

You import it all over the place.

The project grows.

Now you have hundreds of files with MUI.

Then you decide you want a fresh design.

The designers make something amazing.

Now, how are you going to migrate the code? You could:

  • Have a team that now needs to build the new design. But you can’t stop development, so now you get two branches - your source code keeps growing, your design system builders need to chase production.
  • You could solve it by a gradual migration, where you have parts of the UI look old, while some look new.
  • What happens if you want to step away from MUI and move to Mantine? How are you going to deal with the bound logic? How are you going to cope with MUI calling something X and Mantine calling the same Y? And what do you do when the interfaces are different?

And the list goes on, many complexities.

The Solution: Facades

How do I solve it? By using facades.

So let’s take a Button for example. Instead of import { Button } from 'ui-kit-a' and using it directly, I create my own Button component inside the design system folder (preferably a lib in a monorepo, but that’s a different topic).

At first, it doesn’t do anything special:

import { Button as UIKitButton } from 'ui-kit-a';

export interface ButtonProps {
  label: string;
  onClick: () => void;
}

export const Button = ({ label, onClick }) => {
  return <UIKitButton onClick={onClick}>{label}</UIKitButton>
}

Notice how in this example, the UIKitButton expects the label as children. My app will work with ButtonProps that I’ve created, keeping the implementation logic hidden from the App.

Then, if I need to use ui-kit-b for the entire project, or even for a specific component, it’s easy to do.

Keeping It Simple

Another great benefit of this approach is that I never marry a UI kit. I can keep things very simple to keep my bundle size minimal.

Suppose I want to have a Button component. Do I really need the UI kit?

Because using plain HTML and CSS (Tailwind 4 is really nice for that) will most probably do the trick, and that’s as simple as:

export const Button = ({label, onClick}) => {
  return <button className="px-8 py-4 bg-red-400">{label}</button>
}

No need to bring a bunch of JS from a third party lib to maintain a simple Button. Even for complex components it’s not really needed, but more on that in a different post.

Share this article