DEV Community

Andy Maleh
Andy Maleh

Posted on • Updated on

A Look at Rails Hotwire: Turbo Drive

When the Turbolinks technology first came out in Rails 4, some people did not understand what it was really about. I happened to get it right away only because I personally implemented my own version of Turbo Drive for one of my client projects before Turbolinks was released (around 2011), so I really appreciated Turbolinks when it was released. With the latest updates in Rails 7, the Turbolinks technology has been renamed to Turbo Drive (now part of Hotwire) since it not only accelerates hyperlinks, but also form submissions too.

Basically, a very common pattern in jQuery frontend code in web applications is to execute an Ajax call to submit a form or request updated data and then only update parts of the web page that changed.

The most manual way of implementing that is by providing a server-side JSON API resource that returns JSON data and then having the jQuery code receive that data after making an Ajax call and build DOM elements to update parts of the web page manually.

A more streamlined version would be to implement a server-side HTML resource that returns an already rendered HTML partial snippet to use in the jQuery Ajax call to update the part of the web page that changed directly without having to do any DOM building work in JavaScript.

The most streamlined version I personally came up with around 2011 is to simply reuse the same exact server-side HTML resource that rendered the current web page to begin with after making an Ajax call, and then only slice out the HTML elements that changed from the returned full-page HTML and use those elements to update the parts of the web page that changed.

Now, that is almost exactly what Turbo Drive does today so that you do not even have to write jQuery Ajax code or any JavaScript code to begin with. It all happens automatically for you! However, Turbo Drive will by default always replace the entire page's body instead of slicing the parts that changed only. It would have been nice if Turbo Drive automatically performed diffing on pages, but until then, you can use Turbo Frames to solve that problem semi-manually (Turbo Frames are outside the scope of this blog post).

Let's take a look at Turbo Drive in Rails 7:

  1. Install Rails 7:
gem install rails -v7.0.2.3
Enter fullscreen mode Exit fullscreen mode

Output:

Fetching tzinfo-2.0.4.gem
Fetching i18n-1.10.0.gem
Fetching thor-1.2.1.gem
Fetching zeitwerk-2.5.4.gem
Fetching method_source-1.0.0.gem
Fetching concurrent-ruby-1.1.10.gem
Fetching nokogiri-1.13.4-x86_64-darwin.gem
Fetching activesupport-7.0.2.3.gem
Fetching crass-1.0.6.gem
Fetching loofah-2.16.0.gem
Fetching rails-html-sanitizer-1.4.2.gem
Fetching rails-dom-testing-2.0.3.gem
Fetching rack-2.2.3.gem
Fetching rack-test-1.1.0.gem
Fetching erubi-1.10.0.gem
Fetching builder-3.2.4.gem
Fetching actionview-7.0.2.3.gem
Fetching actionpack-7.0.2.3.gem
Fetching railties-7.0.2.3.gem
Fetching mini_mime-1.1.2.gem
Fetching marcel-1.0.2.gem
Fetching activemodel-7.0.2.3.gem
Fetching activerecord-7.0.2.3.gem
Fetching globalid-1.0.0.gem
Fetching activejob-7.0.2.3.gem
Fetching activestorage-7.0.2.3.gem
Fetching actiontext-7.0.2.3.gem
Fetching mail-2.7.1.gem
Fetching actionmailer-7.0.2.3.gem
Fetching actionmailbox-7.0.2.3.gem
Fetching websocket-extensions-0.1.5.gem
Fetching websocket-driver-0.7.5.gem
Fetching nio4r-2.5.8.gem
Fetching actioncable-7.0.2.3.gem
Fetching rails-7.0.2.3.gem
Successfully installed zeitwerk-2.5.4
Successfully installed thor-1.2.1
Successfully installed method_source-1.0.0
Successfully installed concurrent-ruby-1.1.10
Successfully installed tzinfo-2.0.4
Successfully installed i18n-1.10.0
Successfully installed activesupport-7.0.2.3
Successfully installed nokogiri-1.13.4-x86_64-darwin
Successfully installed crass-1.0.6
Successfully installed loofah-2.16.0
Successfully installed rails-html-sanitizer-1.4.2
Successfully installed rails-dom-testing-2.0.3
Successfully installed rack-2.2.3
Successfully installed rack-test-1.1.0
Successfully installed erubi-1.10.0
Successfully installed builder-3.2.4
Successfully installed actionview-7.0.2.3
Successfully installed actionpack-7.0.2.3
Successfully installed railties-7.0.2.3
Successfully installed mini_mime-1.1.2
Successfully installed marcel-1.0.2
Successfully installed activemodel-7.0.2.3
Successfully installed activerecord-7.0.2.3
Successfully installed globalid-1.0.0
Successfully installed activejob-7.0.2.3
Successfully installed activestorage-7.0.2.3
Successfully installed actiontext-7.0.2.3
Successfully installed mail-2.7.1
Successfully installed actionmailer-7.0.2.3
Successfully installed actionmailbox-7.0.2.3
Successfully installed websocket-extensions-0.1.5
Building native extensions. This could take a while...
Successfully installed websocket-driver-0.7.5
Building native extensions. This could take a while...
Successfully installed nio4r-2.5.8
Successfully installed actioncable-7.0.2.3
Successfully installed rails-7.0.2.3
35 gems installed
Enter fullscreen mode Exit fullscreen mode
  1. Create a new rails blog app:
rails new blog_app
Enter fullscreen mode Exit fullscreen mode

Output:

      create  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  .gitattributes
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /Users/andymaleh/code/rails7/blog_app/.git/
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/stylesheets/application.css
      create  app/channels/application_cable/channel.rb
      create  app/channels/application_cable/connection.rb
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/jobs/application_job.rb
      create  app/mailers/application_mailer.rb
      create  app/models/application_record.rb
      create  app/views/layouts/application.html.erb
      create  app/views/layouts/mailer.html.erb
      create  app/views/layouts/mailer.text.erb
      create  app/assets/images
      create  app/assets/images/.keep
      create  app/controllers/concerns/.keep
      create  app/models/concerns/.keep
      create  bin
      create  bin/rails
      create  bin/rake
      create  bin/setup
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/cable.yml
      create  config/puma.rb
      create  config/storage.yml
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
      create  config/environments/test.rb
      create  config/initializers
      create  config/initializers/assets.rb
      create  config/initializers/content_security_policy.rb
      create  config/initializers/cors.rb
      create  config/initializers/filter_parameter_logging.rb
      create  config/initializers/inflections.rb
      create  config/initializers/new_framework_defaults_7_0.rb
      create  config/initializers/permissions_policy.rb
      create  config/locales
      create  config/locales/en.yml
      create  config/master.key
      append  .gitignore
      create  config/boot.rb
      create  config/database.yml
      create  db
      create  db/seeds.rb
      create  lib
      create  lib/tasks
      create  lib/tasks/.keep
      create  lib/assets
      create  lib/assets/.keep
      create  log
      create  log/.keep
      create  public
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/apple-touch-icon-precomposed.png
      create  public/apple-touch-icon.png
      create  public/favicon.ico
      create  public/robots.txt
      create  tmp
      create  tmp/.keep
      create  tmp/pids
      create  tmp/pids/.keep
      create  tmp/cache
      create  tmp/cache/assets
      create  vendor
      create  vendor/.keep
      create  test/fixtures/files
      create  test/fixtures/files/.keep
      create  test/controllers
      create  test/controllers/.keep
      create  test/mailers
      create  test/mailers/.keep
      create  test/models
      create  test/models/.keep
      create  test/helpers
      create  test/helpers/.keep
      create  test/integration
      create  test/integration/.keep
      create  test/channels/application_cable/connection_test.rb
      create  test/test_helper.rb
      create  test/system
      create  test/system/.keep
      create  test/application_system_test_case.rb
      create  storage
      create  storage/.keep
      create  tmp/storage
      create  tmp/storage/.keep
      remove  config/initializers/cors.rb
      remove  config/initializers/new_framework_defaults_7_0.rb
         run  bundle install
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies.......
Fetching rake 13.0.6
Installing rake 13.0.6
Using concurrent-ruby 1.1.10
Using builder 3.2.4
Fetching racc 1.6.0
Fetching minitest 5.15.0
Using erubi 1.10.0
Using crass 1.0.6
Using rack 2.2.3
Using nio4r 2.5.8
Using websocket-extensions 0.1.5
Using marcel 1.0.2
Using mini_mime 1.1.2
Fetching digest 3.1.0
Fetching timeout 0.2.0
Installing racc 1.6.0 with native extensions
Installing digest 3.1.0 with native extensions
Installing timeout 0.2.0
Installing minitest 5.15.0
Fetching strscan 3.0.1
Fetching public_suffix 4.0.7
Installing strscan 3.0.1 with native extensions
Installing public_suffix 4.0.7
Fetching bindex 0.8.1
Installing bindex 0.8.1 with native extensions
Fetching msgpack 1.5.1
Using bundler 2.3.1
Fetching matrix 0.4.2
Installing msgpack 1.5.1 with native extensions
Installing matrix 0.4.2
Fetching regexp_parser 2.3.0
Installing regexp_parser 2.3.0
Fetching childprocess 4.1.0
Installing childprocess 4.1.0
Fetching io-console 0.5.11
Installing io-console 0.5.11 with native extensions
Using method_source 1.0.0
Using thor 1.2.1
Using zeitwerk 2.5.4
Using rexml 3.2.5
Fetching rubyzip 2.3.2
Installing rubyzip 2.3.2
Fetching sqlite3 1.4.2
Installing sqlite3 1.4.2 with native extensions
Using i18n 1.10.0
Using tzinfo 2.0.4
Using rack-test 1.1.0
Fetching sprockets 4.0.3
Installing sprockets 4.0.3
Fetching puma 5.6.4
Installing puma 5.6.4 with native extensions
Using websocket-driver 0.7.5
Using mail 2.7.1
Fetching net-protocol 0.1.3
Installing net-protocol 0.1.3
Fetching addressable 2.8.0
Installing addressable 2.8.0
Using nokogiri 1.13.4 (x86_64-darwin)
Fetching selenium-webdriver 4.1.0
Installing selenium-webdriver 4.1.0
Fetching reline 0.3.1
Installing reline 0.3.1
Using activesupport 7.0.2.3
Fetching net-imap 0.2.3
Installing net-imap 0.2.3
Using net-pop 0.1.1
Fetching net-smtp 0.3.1
Installing net-smtp 0.3.1
Using loofah 2.16.0
Fetching xpath 3.2.0
Installing xpath 3.2.0
Fetching webdrivers 5.0.0
Using rails-dom-testing 2.0.3
Using globalid 1.0.0
Using activemodel 7.0.2.3
Fetching bootsnap 1.11.1
Installing webdrivers 5.0.0
Installing bootsnap 1.11.1 with native extensions
Fetching irb 1.4.1
Installing irb 1.4.1
Using rails-html-sanitizer 1.4.2
Fetching capybara 3.36.0
Installing capybara 3.36.0
Using activejob 7.0.2.3
Using activerecord 7.0.2.3
Fetching debug 1.5.0
Installing debug 1.5.0 with native extensions
Using actionview 7.0.2.3
Using actionpack 7.0.2.3
Fetching jbuilder 2.11.5
Installing jbuilder 2.11.5
Using actioncable 7.0.2.3
Using activestorage 7.0.2.3
Using actionmailer 7.0.2.3
Using railties 7.0.2.3
Fetching sprockets-rails 3.4.2
Installing sprockets-rails 3.4.2
Using actionmailbox 7.0.2.3
Using actiontext 7.0.2.3
Fetching importmap-rails 1.0.3
Installing importmap-rails 1.0.3
Fetching stimulus-rails 1.0.4
Fetching turbo-rails 1.0.1
Installing stimulus-rails 1.0.4
Installing turbo-rails 1.0.1
Fetching web-console 4.2.0
Installing web-console 4.2.0
Using rails 7.0.2.3
Bundle complete! 15 Gemfile dependencies, 73 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
         run  bundle binstubs bundler
       rails  importmap:install
Add Importmap include tags in application layout
      insert  app/views/layouts/application.html.erb
Create application.js module as entrypoint
      create  app/javascript/application.js
Use vendor/javascript for downloaded pins
      create  vendor/javascript
      create  vendor/javascript/.keep
Ensure JavaScript files are in the Sprocket manifest
      append  app/assets/config/manifest.js
Configure importmap paths in config/importmap.rb
      create  config/importmap.rb
Copying binstub
      create  bin/importmap
       rails  turbo:install stimulus:install
Import Turbo
      append  app/javascript/application.js
Pin Turbo
      append  config/importmap.rb
Run turbo:install:redis to switch on Redis and use it in development for turbo streams
Create controllers directory
      create  app/javascript/controllers
      create  app/javascript/controllers/index.js
      create  app/javascript/controllers/application.js
      create  app/javascript/controllers/hello_controller.js
Import Stimulus controllers
      append  pp/javascript/application.js
Pin Stimulus
Appending: pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true"
      append  config/importmap.rb
Appending: pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
      append  config/importmap.rb
Pin all controllers
Appending: pin_all_from "app/javascript/controllers", under: "controllers"
      append  config/importmap.rb
Enter fullscreen mode Exit fullscreen mode
  1. Generate blog post scaffolding:
cd blog_app
rails g scaffold blog_post title:string body:text
Enter fullscreen mode Exit fullscreen mode

Output:

      invoke  active_record
      create    db/migrate/20220418153744_create_blog_posts.rb
      create    app/models/blog_post.rb
      invoke    test_unit
      create      test/models/blog_post_test.rb
      create      test/fixtures/blog_posts.yml
      invoke  resource_route
       route    resources :blog_posts
      invoke  scaffold_controller
      create    app/controllers/blog_posts_controller.rb
      invoke    erb
      create      app/views/blog_posts
      create      app/views/blog_posts/index.html.erb
      create      app/views/blog_posts/edit.html.erb
      create      app/views/blog_posts/show.html.erb
      create      app/views/blog_posts/new.html.erb
      create      app/views/blog_posts/_form.html.erb
      create      app/views/blog_posts/_blog_post.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/blog_posts_controller_test.rb
      create      test/system/blog_posts_test.rb
      invoke    helper
      create      app/helpers/blog_posts_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/blog_posts/index.json.jbuilder
      create      app/views/blog_posts/show.json.jbuilder
      create      app/views/blog_posts/_blog_post.json.jbuilder
Enter fullscreen mode Exit fullscreen mode
  1. Migrate database:
rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Output:

== 20220418153744 CreateBlogPosts: migrating ==================================
-- create_table(:blog_posts)
   -> 0.0039s
== 20220418153744 CreateBlogPosts: migrated (0.0041s) =========================
Enter fullscreen mode Exit fullscreen mode
  1. Start Rails server:
rails s
Enter fullscreen mode Exit fullscreen mode

Output:

=> Booting Puma
=> Rails 7.0.2.3 application starting in development 
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.4 (ruby 3.0.2-p107) ("Birdie's Version")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 26666
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
Enter fullscreen mode Exit fullscreen mode
  1. Visit home page at http://localhost:3000

Image description

  1. Visit blog posts index at http://localhost:3000/blog_posts

Image description

  1. Create 3 blog posts:

Image description

  1. Open the web browser developer tools and go to the Network tab:

Image description

  1. Show a blog post and notice how only a few network calls were made because of Turbo Drive without refreshing all the page resources:

Image description

  1. Refresh the page in the web browser and notice how all resources got loaded in the Network tab:

Image description

  1. Edit the blog post and clear the Network tab (click on the second button in the second row at the top of the developer tools to do so):

Image description

  1. Update the blog post by submitting the form and notice how only a few network calls have been made because of Turbo Drive without refreshing all the page resources:

Image description

And, that's all folks!

Learn more about Rails 7 Hotwire Turbo Drive in the Turbo Handbook.

Discussion (0)