DEV Community

thomasvanholder
thomasvanholder

Posted on • Updated on

How to upgrade/migrate turbolinks to hotwire

  1. Replace stimulus imports
  2. Replace turbolinks namespaces with turbo
  3. Disable remote forms
  4. Render form errors
    1. Forms on get routes
    2. Forms in modals
  5. Replace .js.erb to turbo_stream.erb

With the upcoming Rails 7 release, hotwire and stimulus will be the default JavaScript set-up for any new Rails project. To get existing rails application up to speed with the newest ste-up, the guide below can help you migrate from turbolinks to hotwire.

A note on Devise and Rails UJS

To use devise with hotwire, there are a few changes needed. Check the GoRails episode for a practical guide.

For now, Rails UJS can still co-exist with Hotwire. The guide below is built with the assumption Rails UJS is installed. If you look to say goodbye to UJS completely, check this Drifting Ruby episode.

Installation

Install the gem hotwire-rails. The gem is a wrapper which will install:

  • Turbo (@hotwired/turbo-rails")
  • Stimulus JS library (@hotwired/stimulus)

If you have already installed stimulus, remove the package using yarn or npm. Ex: yarn remove stimulus@^2.0.0.

Replace

# old
gem 'turbolinks'
# new
gem 'hotwire-rails'
Enter fullscreen mode Exit fullscreen mode

Run ./bin/bundle install
Run ./bin/rails hotwire:install

Remove any turbolinks reference in application.js

// old
require("turbolinks").start()
// new 
import "@hotwired/turbo-rails"
Enter fullscreen mode Exit fullscreen mode

1. Replace stimulus imports

If you already had Stimulus JS installed, you'll need to update your existing stimulus controllers. Set the import library to @hotwired/stimulus.

// old
import { Controller } from "stimulus"
//new 
import { Controller } from "@hotwired/stimulus"
Enter fullscreen mode Exit fullscreen mode

2. Replace turbolinks namespaces with turbo

Check if your app contains turbolinks references and it swap for the new turbo namespace.

For example:

  • script tags
// old
document.addEventListener('turbolinks:load', ...)
// new
document.addEventListener('turbo:load', ...)
Enter fullscreen mode Exit fullscreen mode
  • turbolinks opt-outs for specific link clicks
<!-- old -->
<%= link_to path, data:{turbolinks: false} do %>
<!-- new -->
<%= link_to path, data:{turbo: false} do %>
Enter fullscreen mode Exit fullscreen mode
  • programatically redirect Turbolinks.
# old
Turbolinks.visit(location)
# new
Turbo.visit(location)
Enter fullscreen mode Exit fullscreen mode

3. Disable remote forms

Set the config in application.rb as mentioned in the turbo-rails to disable all remote forms.

config.action_view.form_with_generates_remote_forms = false
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can change every form, one by one, by adding local: true.

<!-- old -->
<%= form_with model: @task, url: task_path(@task), method: :put do %>
...
<% end %>

<!-- new -->
<%= form_with model: @task, url: task_path(@task), method: :put, local: true do %>
...
<% end %>
Enter fullscreen mode Exit fullscreen mode

4. Render form errors

4.1. Forms in GET routes

like on /new or /edit page

Add status: unprocessable_entity when rendering the form in the else-leg to render errors on the page.

# old
def create
  @task = Task.new(task_params)
  if @task.save
    redirect_to tasks_path, notice: 'Task created.'
  else
    render :new
  end
end

# new
def create
  @task = Task.new(task_params)
  if @task.save
    redirect_to tasks_path, notice: 'Task created.'
  else
    render :new, status: :unprocessable_entity
  end
end
Enter fullscreen mode Exit fullscreen mode

4.2 Forms in modal

Using the :unprocessable_entity approach in step 3.1 won't work for forms displayed in modals. This GoRails episode details an in-depth solution. Use the new turbo_stream format to render errors instead of status: :unprocessable_entity. The turbo_stream.erb will find the form id and replace it with a new HTML template.

This is also a good time to replace existing js.erb templates to the new turbo_stream format.

# old
def create
  @task = Task.new(task_params)
  if @task.save
    redirect_to tasks_path, notice: 'Task created.'
  else
    render :new
  end
end

# new
def create
  @task = Task.new(task_params)
  if @task.save
    redirect_to tasks_path, notice: 'Task created.'
  else
    respond_to { |format| format.turbo_stream }
  end
end
Enter fullscreen mode Exit fullscreen mode

Next, add an id to the modal form. Read about the dom_id helper here.

<!-- old -->
<%= form_with model: @task) do |f| %>
...
<% end %>
<!-- new -->
<%= form_with model: @task, id: dom_id(@task) do |f| %>
...
<% end %>
Enter fullscreen mode Exit fullscreen mode

Finally, add a new file in the view folder that matches the action name of the controller: create.turbo_stream.erb

<%= turbo_stream.replace "new_task", partial: "tasks/form", task: @task %>
Enter fullscreen mode Exit fullscreen mode
  • new_task refers to the id of the form that is generated by dom_id(@task).
  • "tasks/form" is the same form partial that initially displays the form in the modal
  • @task is the updated instance with errors which is passed along to the form

5. Replace .js.erb to turbo_stream.erb

Using hotwire and its alternative approach to update content on your page. You want to benefit from the new turbo_stream approach.


Thanks for reading.

Questions? Feedback? Let me know :)

Top comments (2)

Collapse
 
r3id profile image
Alan Reid

Nice article, thanks for sharing.

Collapse
 
mpetricone profile image
mpetricone

Thanks!