[Rails, Hotwire] Migrate to Turbo from rails-ujs and turbolinks – how it went for us.

We recently decided to migrate one of our newest platforms to Turbo. The goal of this article is to help anyone who plans to do the same migration. I hope it gives you a perspective of the amount of work required. Generally it was easy and straightforward, but a few specs had to be changed because of urls and controller results

Gemfile

Remove turbolinks and add turbo-rails. The change was

--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -227,9 +227,8 @@ GEM
     switch_user (1.5.4)
     thor (1.1.0)
     tilt (2.0.10)
-    turbolinks (5.2.1)
-      turbolinks-source (~> 5.2)
-    turbolinks-source (5.2.0)
+    turbo-rails (0.7.8)
+      rails (>= 6.0.0)

application.js and no more rails-ujs and Turbolinks

Added “@notwired/turbo-rails” and removed Rails.start() and Turbolinks.start()

--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -3,8 +3,7 @@
 // a relevant structure within app/javascript and only use these pack files to reference
 // that code so it'll be compiled.

-import Rails from "@rails/ujs"
-import Turbolinks from "turbolinks"
+import "@hotwired/turbo-rails"
 import * as ActiveStorage from "@rails/activestorage"
 import "channels"

@@ -14,8 +13,6 @@ import "channels"
 // Collapse - needed for navbar
 import { Collapse } from 'bootstrap';

-Rails.start()
-Turbolinks.start()
 ActiveStorage.start()

package.json

The change was small

--- a/package.json
+++ b/package.json
@@ -2,10 +2,10 @@
   "name": "platform",
   "private": true,
   "dependencies": {
+    "@hotwired/turbo-rails": "^7.0.0-rc.3",
     "@popperjs/core": "^2.9.2",
     "@rails/actioncable": "^6.0.0",
     "@rails/activestorage": "^6.0.0",
-    "@rails/ujs": "^6.0.0",
     "@rails/webpacker": "5.4.0",
     "bootstrap": "^5.0.2",
     "stimulus": "^2.0.0",

Device still does not work

For the device forms you have to add “data: {turbo: ‘false’}” to disable turbo for them

+<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }, data: {turbo: "false"}) do |f| %>;

We are waiting for resolutions on https://github.com/heartcombo/devise/pull/5340

Controllers have to return an unprocessable_entity on form errors

If there are active_record.errors in the controller we must now return status: :unprocessable_entity

+++ b/app/controllers/records_controller.rb
@@ -14,7 +14,7 @@ class RecordsController < ApplicationController
     if @record.save
       redirect_to edit_record_path(@record)
     else
-      render :new
+      render :new, status: :unprocessable_entity
     end
   end

application.js was reduced significantly

The old application.js – 923 KiB

  application (932 KiB)
      js/application-dce2ae8c3797246e3c4b.js

The new application.js – 248 KiB

remote:        Assets: 
remote:          js/application-b52f4ecd1b3d48f2f393.js (248 KiB)

Conclusion

Overall a good experience. We are still facing some minor issues with third party chat widgets like tawk.to that do not work well with turbo, as they are sending 1 more request, refreshing the page and adding the widget to an iframe that is lost with turbo navigation. But we would probably move away from tawk.to.