On Starting A Side-Project: Hotwire vs. Angular
For the last few months, I've been digging into the Hotwire framework. I was initially drawn to Hotwire on its promise of allowing me to build a SPA (Single-Page Application)-like experience using an MPA (Multi-Page Application); and, to do so with less effort. After several months of creating demos and migrating this ColdFusion blog over to using Hotwire, I feel like I have a much better sense of how Turbo Drive, Turbo Streams, and Stimulus work. But, I'm not quite sure that I want to use Hotwire when I start my next side-project.
CAUTION: This blog post is very much me talking to myself, trying to figure some things out.
I'm Happy That I Updated My Blog to Use Hotwire
I used this blog as a laboratory for learning about Hotwire. Prior to this, I had no framework at all. Sure, I had jQuery, which I eventually replaced with Umbrella JS; but, there was nothing really tying everything together. As such, moving to Hotwire somewhat forced me to organize my code better.
Plus, I do love the fact that Hotwire creates a persistent process, which reduces the amount of work each subsequent navigation event has to incur. Of course, this is a "blog" where the number of pages viewed is "1" in the vast majority of cases; so, the relevance of a "subsequent navigation event" is a matter of much debate.
In the end, if I were to build another public facing "content" site, I do believe that I would choose Hotwire. I also believe that I might choose it for any "simple" back-end administrative system.
Hotwire Doesn't Address Some Important Problems
I understand that a lot of the uphill battle with Hotwire - with anything new - is lack of experience. I've been building-up a completely new mental model, and it's not always obvious how things are supposed to be done. But, after a few months of experience, I'm beginning to see that Hotwire doesn't really address a certain set of problems. Here are some things that still seem fuzzy and unanswered in my mind:
Scoping of Names: Hotwire uses the DOM (Document Object Model) as the source of truth; and, maps DOM attributes to Stimulus Controllers. As such, every controller has to be uniquely named. As an application grows and evolves, I suspect that Controller names will have to get longer and more complex in order to remain unique. And, since controller names are included in every "action", "param", and "target" target as well, the DOM is going to get wordy!
ASIDE: I've see this with other Dependency-Injection (DI) frameworks as well. In fact, in my AngularJS 1.x apps, I've started to prefix my Component tag-names with
xis a globally-unique counter. Example
<m97-email-recipients>. This way, I can avoid naming collisions without having to create artificially complex names.
Scoping of CSS: Most of the Hotwire examples on the web seem to use Tailwind CSS. And, I suspect that some of the impetus behind this is a lack of any native CSS scoping. Since there's no inherent compilation step in a Hotwire application, there's no hook that allows Hotwire to inject template modifications that enable scoping.
Encapsulated Interfaces: While reusing templates for rendering is a core part of the Hotwire way, this doesn't directly address the need to encapsulate complex interfaces. Ruby on Rails has some helpers for this; but, if you're not using Rails, you're on your own.
Layered Routing: If your application is "flat," in that a URL route only ever maps to a single page, Hotwire "just works". But, if you want to start using "auxiliary routes" to represent page state - such as making a modal window deep-linkable from anywhere in the application - there's no a great native way to do this. You can use Turbo Frames with an
advanceaction to change the URL; but, this doesn't really address the page-refresh issue; or, the need to simply close the modal window and revert back to the previous URL.
Error Handling: Whether it be a
401 Unauthorizederror or a
500 Server Error, handling failures in Hotwire is non-obvious. You can hook into events and override rendering; however, since Hotwire is doing all the fetching for you, there's no clear place to put this kind of logic. And, since Turbo Frames and top-level requests seem to fail in different ways, handling errors is all the more complex.
Now, again, I have to stress that I only have a few months experience with Hotwire; so, take this all with some healthy apprehension. Plus, the Basecamp team has built Basecamp and HEY, both of which are highly dynamic, interactive applications. So, if you really know what you're doing, it's clear that Hotwire gives you the tools to get it done.
Is Hotwire Really Less Work?
Part of the Hotwire messaging is that building an application is going to be less work. But, I'm not sure it actually works-out that way. I believe that, more than anything, aspects of the control-flow just live in different places. But, I don't believe that there's actually less stuff to build.
Consider a "Contact" form. With a Contact form I need:
- A controller action to render the form.
- The form HTML itself.
- A controller action to receive the submission.
- The business logic to process the submission.
- A controller action to render the Thank you.
- The Thank you HTML itself.
With Hotwire, that's all done server-side. Without Hotwire, some of that is server-side and some of that is client-side. Furthermore, the server-side parts are likely spread out across page-controllers and API-controllers. But, there's nothing here that I would remove when using Hotwire; nor is there anything that I would add when using a single-page application. It's the same stuff, it's just in different places.
Of course, if you're using Hotwire because you get to use the server-side technology more often, then it's not the volume of "stuff" that matters, it's the implementation of the stuff that makes the difference for you.
Hotwire's "Back Button" is Fundamentally Better
One thing that Hotwire clearly gets right is the Back Button. When you hit the back button in a Hotwire application, Turbo Drive just pulls the page out of the cache and renders it instantly. This is something that no Single-Page Application can really do quite as well (or so it seems).
Hotwire Leads to More Reusable Widgets
If I were to choose Angular for my next side-project, then all the widgets I built would end up working in an Angular-only context. Which means, if I had something special - like a "Fancy Select Box" - I couldn't use it on any View outside of the Angular app.
With Hotwire, on the other hand, since everything is based on server-side partial reuse, any widget can be used anywhere (as long as you're loading Hotwire). Well, I mean, sort of - it depends on whether that widget was built to work via progressive enhancement.
It's All About Trade-Offs
There's no clear-cut winner here. The reason I'm even writing this post is because I'm conflicted; and, the writing helps me think-through the problem. I think there are some things that Hotwire does really well; and, I think there are some points of friction. The question then becomes, are the benefits worth the drawbacks?
Much to consider!
The other relatively large drawback to using Hotwire is that it doesn't working with
.cfmfile extensions natively. Which means, any site that uses Hotwire will also have to use some sort of URL-rewriting that maps all
.cfmrequests. This isn't a huge deal; but, it does add another layer of complexity where I have to define route-definitions. If I use Angular, it doesn't matter what the URLs look like.
Thank you, Ben, for this honest article. Some answers:
Naming stimulus controllers: I wrote https://www.npmjs.com/package/vite-stimulus-initializer. The goal is to prevent spaghetti names from getting longer and longer, and it throws an exception if there are naming conflicts.
Long spaghetti strings for Stimulus properties: Stimulus has one strength: Its initialization process, specifically the connect function that runs after a Stimulus controller reaches the surface. For me, this is the only use case for Stimulus. For components that are a bit more complex, in my opinion, and for turbo rails, the only answer is: Svelte as a custom element, but without shadow dom, as explained in my tutorial, link below.
CSS Scoping / Tailwind Why do so many people use Tailwind? Tons of classes for every case!! Classes are much better organized in frontend frameworks like bootstrap or zurb foundation. The latter is my favorite because it is the slimmer one.
Layered Routin / page refresh issue check turbo_power gem, may this solve some?
Is Hotwire Really Less Work?
Have a look at https://rubygems.org/search?query=render_turbo_stream. There are so many details that I spent months searching the world wide web and finally wrote this gem. Now I am optimistic that the answer can be a resounding yes. I see the drawback of separating frontend and backend and having to separate larger test libraries.
Angular has advantages over hotwire, but the same advantages are provided by https://svelte.dev/, which integrates well with hotwired / rails, see https://dev.to/chmich/setup-inertia-and-svelte-on-rails-7-3glk.
Your conclusion is: "There is no clear winner here". I am not familiar with Angular, but I have a feeling about it. I would be very curious if you would take some hours of your precious time, build an app based on https://dev.to/chmich/setup-vite-svelte-inertia-stimulus-bootstrap-foundation-on-rails-7-overview-1bk1 and https://rubygems.org/gems/render_turbo_stream. May be there are solved many details you described above.
It looks like you're really laying down a good foundation for solving problems in a Hotwire context. I'm only vaguely familiar with how Ruby / Rails work, so it's not super easy for me to follow your gem code; but, I think I get the gist of it. You're essentially removing a lot of the boiler-plate (if I understand correctly) for rendering streams. I think that makes a lot of sense. I've also been trying to do some similar things on my end using "layouts".
I'm sure this is possible; but, it's not simple and it's not easy to keep both worlds in your head at the same time.
I sometimes wonder how I would approach Hotwire if I didn't think about progressive enhancement at all. That might completely change the way I wire things together. At least when I'm using something like Angular, I know that progressive enhancement is not possible. So, it removes that whole conversation / distraction from the table.
How do you approach / think about progressive enhancement?
The wiz bang feature IMO of Hotwire is Turbo Streams.
The idea of being able to re-render islands of content on the page and deliver fresh content to the client all from the same cfml template. No need to ship json to the client and have view specific code to unpack it and update UI elements. Using such a feature in CFML would account for 90%+ of the reactivity my business apps would need.
It makes be wonder do I want to to a full implementation of Hotwire in CFML or do I just want to be inspired by the pattern. Then in my next project just lightly implement a turbo-streams like feature in native cfml?
That's actually a really fascinating idea! I agree that the Turbo Stream stuff is really cool. It took me a while to start to wrap my head around it, and to figure out how this kind of stuff dove-tails with the application control flow. But yeah, being able to re-render chunks of HTML is really nice. And, if look at the implementation details for how Turbo Streams work, it's really just a bunch of
.prepend()calls. I mean, there's a lot of infrastructure that goes around those calls; but, there's not really much "magic" to the Turbo Stream mechanics.
One thing that really kills me (about Hotwire) though is that it won't work with
.cfmextensions, at least not currently. If I'm building a "content" site (like this blog), it's not really a problem to proxy everything through
.htm. But, if I was building an "app", I think it would be much more frustrating having to have URL mappings.
I really really wish it would allow me allow-list file-extensions for Turbo Drive.
Hi pleasant guys on that page :)
Thanks for your comment.
Now I have released version 4 of render_turbo_stream and I hope that at least my gem code is easier to follow. 4 major releases in one month! Finding a consistent naming convention was not easy, but I think the groundwork is done now.
And To the original question: «Is Hotwire really a time saver?» I am now convinced that the answer is yes!
developer ergonomics. Svelte: The best I know. Rails: I love it! Turbo: In a very early stage. For the latter i wrote my gem.
Svelte in Hotwire with custom elements: The initialization process is, in my opinion, absolutely stable. I see no need for a «fallback to a native element», but also no way. If you would go with react, the most used frontend lib in the world, it does nothing else and it works. The best thing i found is the way I described in my tutorial, together with turbo: The initialization step happens on the very first page load! Then the element is built on the front, waiting to work for you. If you push this element to daylight at any later time with turbo, with any parameters, there is no more initialization step needed, this is the point that makes the work easier for you as a developer and a faster experience for the user.
@Peter What you wrote is the idea of Hotwired and its independence from Rails. I am not familiar with CFML, so I feel I cannot answer your question, but what I see at first glance: CFML brings server code together with presentation code, and on the other hand, helps to separate them. Rails has embedding languages like .html.erb or .haml (which I use). They are really handy and necessary to put logic in views. But we all try to reduce the logic in views as much as possible. Exactly that is the approach of my gem. So I feel like we are a little bit on the same path, but I cannot say how CFML and Hotwired work together.
If you want to integrate Hotwired in a framework other than Rails and if you want to have a complete workflow along with a complete testing strategy:
The frontend part should be easy, just follow hotwired.
In the backend part you can read the READMEs of the gems turbo-rails and render_turbo_stream. Check what is relevant for you and solve this part in another way.
Yeah, I'm still struggling quite a bit with the progressive enhancement stuff. I can absolutely see a way to follow a progressive mindset with a relatively simple view (like a "normal" web site). But, it just seems like building will become increasingly complex and the app becomes increasingly robust.
One of these days, I should sign-up for Hey to see how they did it.
Yes, in fact this exactly is the challenge. If it is true that DHH is the or one of the first which really is going in this direction, like seems a little bit from this words «Fullfilling a vision» then its clear that we are in a early state and in a early state live is not always komfortable.
I am happy now, but I know how many details I had to solve the last months. I am curious which way you will choose next time.
Best regards, Chris
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →