David Heinemeier Hansson

June 15, 2021

Bringing Hotwire to Basecamp

Hotwire is now powering Basecamp 3 on the alpha version we're running internally, and I thought it'd be helpful to document the upgrade process. Although upgrading is perhaps a big word. It's not like we rewrote all the JavaScript we have to make this happen. Coexisting is probably a better term. While existing JavaScript code stays as it is, we're now set for all our future development to use Turbo Frames, Turbo Streams, and Stimulus 2.0. Yay!

Basecamp 3 was originally built with Rails UJS, Turbolinks, and Stimulus. In our new setup, Rails UJS is still there to handle data-remote/data-method forms and links for the legacy code, but it leaves alone any link or form tagged with data-turbo=true. Making this so required an upgrade to the UJS event handler selectors, which will be part of the next Rails release. And if you are still using rails/jquery-ujs (like Basecamp!), there's a new version 1.2.3 with the updated selectors as well.

It also required that all these new forms and method links we want flowing through Turbo instead of Rails UJS be tagged with that data-turbo=true attribute. Something you don't need to do if you're starting with Turbo from the beginning, like we did with HEY, but it's a small price to pay for coexistence.

The upgrade to Stimulus 2.0 (from 1.1) took a little more effort, but solely because we wanted to upgrade early to the new target format of data-[identifier]-target="[name]" instead of data-target="[identifier].[name]" (to get rid of the deprecation warnings). Although it was easy enough, I still managed to wrongly turn a few of these conversions into duplicate data-[identifier]-target attributes. So pay attention to that.

The meat of the coexistence work was in replacing Turbolinks with Turbo. Turbo Drive is a direct continuation of Turbolinks, but it still required a fair bit of work to update all the namespaced events, and ensure that the Ajax requests coming from Rails UJS would play nice with 302 redirects. That was something the old turbolinks-rails gem included in the box, but turbo-rails does not (since it's not needed for greenfield work). There was enough steps in that process to warrant documenting them in the turbo-rails repository.

As part of this work, I caught up with both the Turbo and Stimulus repositories. I released a couple of new versions of Turbo/Turbo Rails, which both fixed a number of bugs, but also gave us some great new powers, like before/after actions, built-in method links, and deduping of existing elements on the append/prepend actions. We're up to beta 7 now, and will keep going on the beta process for a bit longer while clearing out the backlog of pull requests and issues, but then it'll soon be time for a proper 7.0 final release as well.

Stimulus is also due for a 2.1 release in the near future, which will include a helpful debug mode, previous value passing on the ValueChanged callbacks, default values, and a few other overdue enhancements.

This kind of upgrade work is something it's so easy to put off for existing applications. What you have already works, right? But if you keep doing that forever, you eventually end up with a codebase that's so far from the state of the art that it won't be any fun to work in it.

It's not that you have to rewrite everything from scratch all the time, but taking the effort to ensure that new code can be written to the best of your abilities is key to enjoying the work. And as you extend existing features, follow the principle of leaving the campsite better than you found it. For Basecamp, that means rewriting any of that old jquery code when we're doing substantial work around features using it.

As the baboon said: "It gets easier. Every day it gets a little easier. But you got to do it every day. That's the hard part. But it does get easier."


About David Heinemeier Hansson

Made Basecamp and HEY for the underdogs as co-owner and CTO of 37signals. Created Ruby on Rails. Wrote REWORK, It Doesn't Have to Be Crazy at Work, and REMOTE. Won at Le Mans as a racing driver. Fought the big tech monopolies as an antitrust advocate. Invested in Danish startups.