Rails Components: More Installer Over Library

After covering what rails components are and why they need to be first class citizens in the rails ecosystem, in the next few posts I'm going to share my vision for how they could work (and for the most part, how they currently work in the first component library, shadcn on rails).

Originally I was going to cover everything about how they work in one post, but as you've probably surmised if you've been reading, I tend to err on the verbose side and have no qualms about waxing philosophical about code. So, rather than try to cover everything about how the libraries work, I want to cover one major design decision about them.

tl;dr by the end of this post, I actually argue myself into indecision and remain unsure of whether to make the gems library dependencies and installers or just installers. In fact, I'm leaning towards dependency and installer. 🤷

Installer More than Library

Installation

The library that really inspired all this, shadcn has an interesting philosophy when it comes to component libraries as dependencies. More to the point, the library aims to not be a dependency itself but rather boilerplate that you install into your application, take over, and use as a base for your own internal component library and design system. From the site:

This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps.

What do you mean by not a component library?

I mean you do not install it as a dependency. It is not available or distributed via npm.

Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours.

Use this as a reference to build your own component libraries.

I really liked that idea. I tend to think that dependency bloat, especially in production applications (as opposed to your greenfield weekend project), is a real thing. We're spoiled in the Ruby world that bundler and RubyGems are so awesome and reduce a lot of the problems of say, NPM. Philosophically I think that dependencies should be exactly that, dependencies, external packages your application actually 100% depends on and could not function without.

omniauth is a dependency, it's providing you with an entire infrastructure for accomplishing a specific task that is fully encapsulated and you would never want to change within your application (you'd only want to use it by at most extending it).

To me, a component library is not a dependency, it's a collection of code that you want to be able to change and customize to your needs. It's not a black box, in fact using it means exposing its internals.

The first big decision I've made regarding the rails component libraries I'm authoring is fully embracing the idea of installer over library. Including the shadcn-ui gem only adds rails generators to your application, not the helpers or views that make the components.

So bundle add shadcn-ui doesn't actually give you any component in the library, it just gives you a generator to install any component you want into your application. It's not mounting the helpers to your applications scope. Its similar to how devise let's you generate the controller and views it uses into your application so you can customize them but different in that you can't use them at all if you don't generate them into your application.

rails generate shadcn-ui alert

That's how you use the gem. That command will copy the required files, a full unit with no external dependencies, into your application (generally only 3 files). Once installed, the code is yours to customize and maintain.

If you update the gem and there's a new version of the component, you could re-install it into your application, but it would mean overwriting the files previously installed, which is potentially an issue I'll discuss later.

In speaking to a few people, this seems to be a controversial choice. It would be nothing to make the gem include the views and helpers in your application without copying files and also providing the installer SHOULD you want to customize them. Basically exactly like devise works. So, I could give people the choice to use the gem as a traditional library dependency or as an installer. Instead, for now, I've chosen to enforce installer over library.

Why?

Well I guess this line really spoke to me "Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours." I like the idea of your application actually not having the code or access to the code for components you aren't using. While off the top of my head I can't think of any issues with that, it's not that much code, certainly by simply not putting that in your applications space, there won't be any conflicts or weird behavior associated with having 50+ components included in your application when you're only using 10.

"Use this as a reference to build your own component libraries." I think by mandating the installer approach, the library is enforcing a philosophy. These components are based on the originals but are now yours and are your responsibility. They bootstrap your apps design system, but you must implement and take ownership over them. The reality is they are going to need to be customized. They should be otherwise we're going to get a lot of really homogenous designs.

I believe that tools influence their usage and influence how we think and how we approach problems. By crafting rails components as installers over dependencies, I'm trying to get you to think about your components and not take them for granted or relegate them again to citizens outside the domain of your application and framework.

Upgrading and Version Drift

Another issue people have pointed out with this approach is that it makes upgrading components more difficult. If you've customized a component and a new version comes out, you have to manually merge the changes into your customized version. Now that's not 100% true because it really depends on how you customized them. If you extended them, say by wrapping them in a new helper method or by wrapping them in a ViewComponent, then as long as the API is maintained, upgrading by overwriting the original installed files isn't a problem.

But if you've customized the base main helper methods or the main component partial, there will be version drift and merge conflicts and overwriting the files for the upgrade would mean losing your customizations, which I won't do. This sounds like a big problem but I think in reality, it is not, or it hasn't proven to be in my usage of the original React shadcn library.

One, I tend to extend the components in their usage and composition with each other more than want to edit the individual files. Though saying that out loud makes me wonder why expose the source of the components then to the application at all through installation over dependency. Two, when I've customized the source component files, the upgrades tend to either be easy to see and integrate with basic diffing or something I don't care about because the component works well enough in my application. However, those are most likely not going to be true universally.

Or Maybe Not

Look, to be honest it's a very good argument for being flexible and allowing the dependency to be inherited while also including the installer. To be totally honest, by the end of writing this post, I'm still undecided. Given bundler and rubygems, there seems less harm here and perhaps I'm being needlessly ideological. I would really love to hear your opinion as by a 1.0 release of the first two libraries, I want them working the same and cementing these crucial initial design decisions that are one way doors that are going to be hard to change in the future. Please reach out to me with your opinions or confusions, @aviflombaum.

Did you find this article valuable?

Support Avi Flombaum by becoming a sponsor. Any amount is appreciated!