Skip to the content.

Rails Turbo

Turbo Drive

Turbo Drive is mostly what Turbolinks was earlier. Link clicks and form submissions are automatically intercepted, and instead of a full page load, Turbo loads the target using fetch and replaces the <body> element. This prevents the browser from needing to reparse CSS and JS resources.

To disable Drive for a single element (a link or a form), annotate it with data-turbo="false".

Turbo Frames

Turbo Frames allows developers to replace only parts of a page when the user clicks a link or submits a form. This can be used to show an inline form in a list of editable items.

To use turbo frames, use the turbo_frame_tag helper and pass the ActiveRecord entity to the helper:

todos/index.html.erb

<% @todos.each do |todo| %>
  <%= turbo_frame_tag todo do %>
    <div><%= todo.title %> <%= link_to 'edit', edit_todo_path(todo) %></div>
  <% end %>
<% end %>

All navigations that lead to a page containing the same tag will automatically be injected into the existing frame. Note however, that Rails still renders the entire template, and the frontend JS extracts the tag and inserts it into the appropriate place. This might have performance implications if the page is particularly expensive to generate.

todos/edit.html.erb

<%= turbo_frame_tag @todo do %>
  <%= simple_form_for @todo do |f| %>
    <%= f.input :title %>
    <%= f.submit %>
  <% end %>
<% end %>

Editing an item of the list will inject the form into the list. Saving the form will replace the form with the updated list item.

⚠ The target page must always include the turbo_frame_tag, otherwise an error message (“Content missing”) will be displayed.

Turbo Streams

Streams are used to replace parts of a website after a user action. In addition to the Frames above, they can be used to

Contrary to the Frames, Streams only render the part to replace, and not the whole page. This can make rendering a lot faster, if the part of the website to update is small and the whole page is large.

To deliver an update from the backend, you can choose how the update should be treated on the frontend. You can either append or prepend a new item to a container, you can replace an existing item (losing all event handlers of the item), update the content of an existing item (keeping the handlers of the item), remove an item, or add an item before or after a specific other item.

Local updates

To update a part of a website in response to a user action, add a turbo_stream format handler to the controller action:

todos_controller.rb

def create
  todo = current_user.todos.create!(params.require(:todo).permit(:text))

  respond_to do |format|
    format.turbo_stream { render turbo_stream: turbo_stream.append(:todos, partial: "todos/todo", locals: { todo: todo }) }
    format.html         { redirect_to todos_url }
  end
end

def destroy
  todo = current_user.todos.find(params[:id])
  todo.destroy

  respond_to do |format|
    format.turbo_stream { render turbo_stream: turbo_stream.remove(todo) }
    format.html         { redirect_to todos_url }
  end
end

For the create action, this automatically renders todos/todo with the todo local variable and appends it to the element with id “todos”. Note that the following calls are equivalent, but partial and locals can be customized:

For the delete action, this automatically removes the element with the dom_id(todo). Instead of a model (on which to_key() is called), it’s also possible to pass a specific DOM id.

It is also possible to apply multiple updates by passing an array like [turbo_stream.remove(todo), turbo_stream.update(notification)]

Broadcasts

To broadcast changes, visitors must subscribe to a specific channel in order to get the updates. This allows developers to scope the updates to specific topics or specific (authorized) users.

To subscribe to a stream on the frontend, use the turbo_stream_from tag and pass an identifier.

todos/index.html.erb

<%= turbo_stream_from "todos" %>

Any user subscribing to the same stream will get all updates to this stream.

It is also possible to scope a stream more narrowly, by passing multiple identifiers:

<%= turbo_stream_from current_user, "todos" %>

The stream name is derived from all arguments to the turbo_stream_from call. That means that messages are now scoped to the user.

Updates can be either sent as a result of a model change:

class Todo < ApplicationRecord
  broadcasts_to ->(todo) { [todo.user, "todos"] }
end

or in a controller, by custom code:

def create
  ...
  @todo.broadcast_append_later_to(current_user, "todos")
  ...
end

This instructs Rails to broadcast all updates to the users subscribed to the channel {user}, "todos". The model call to broadcasts_to automatically defines a default behaviour for updating the UI when a new record is created (calling append), updated (calling replace) or destroyed (calling remove). For the controller broadcasts, the action must be specified in the broadcast call. The defaults are as follows and can be overriden:

Property Default Overridable by
ID of the container to update #todos (model plural) target: "all_todos"
Create action Append to container inserts_by: :prepend
Generated partial todos/_todo (model default partial) partial: "todos/simple_todo"
Stream name [todo.user, "todos"] -

Custom model actions

Model updates can also be delivered at any other time in the lifecycle and by applying a custom action:

class Todo < ApplicationRecord
  after_create_commit :broadcast_later

  private

  def brodcast_later
    broadcast_prepend_later_to user, :todos
  end
end

The payload is the partial derived from the model name (todos/todo) and will be prepended to the DOM element which has the plural name of the model as the ID (todos).