Skip to main content

Turbolinks and Turbo Support

React on Rails Updated to support Turbo, August 2024​

Using Turbo​

Turbo is the modern replacement for Turbolinks, providing fast navigation through your Rails app without full page reloads.

Basic Setup​

1. Install Turbo

Add the Turbo Rails gem and JavaScript package:

# Gemfile
gem "turbo-rails"

# JavaScript
yarn add @hotwired/turbo-rails
# or: npm install @hotwired/turbo-rails
# or: pnpm add @hotwired/turbo-rails

2. Enable Turbo in React on Rails

Import Turbo and configure React on Rails to work with it:

// app/javascript/packs/application.js
import '@hotwired/turbo-rails';

ReactOnRails.setOptions({
turbo: true, // Enable Turbo support (not auto-detected)
});

3. Use Turbo Frames

With turbo: true set, React on Rails mounts and unmounts the components inside a Turbo Frame as it navigates, subject to the requirements below:

<%# app/views/items/index.html.erb %>
<%= turbo_frame_tag 'item-list' do %>
<%= react_component("ItemList", props: @items) %>
<% end %>

<%# Clicking a link that responds with another turbo_frame_tag will update just that frame %>

When a frame navigation replaces the frame's content, the react-on-rails package handles turbo:frame-render:

  1. It unmounts the components the render removed from the page, so their React roots do not leak.
  2. It mounts the components in the new frame content.

Nothing is unmounted before the frame renders, so pausing or canceling turbo:before-frame-render (for example, to animate the frame) leaves the old components working.

Components outside the frame stay mounted. Redux stores are page-wide: a new component rendered with declared store dependencies initializes only those stores, and one without them initializes every store on the page. A store the page already has keeps its state.

Turbo inserts only the frame's markup from the response, so anything the response renders outside the frame is discarded. That leads to three requirements:

  • Bundles: frame components must be registered by JavaScript the page already loaded. Pack tags in the frame response, including the ones auto_load_bundle appends, are not run. Load those packs on the page that holds the frame.
  • Rails context: React on Rails writes the js-react-on-rails-context element next to the first component a response renders. The page must already have one, or the frame response's first component must be inside the frame.
  • Stores: a store a frame component reads must already be on the page or have its hydration data rendered inside the frame. Store data that redux_store_hydration_data writes elsewhere in the layout is discarded.

With the react-on-rails-pro package, each component's inline script mounts the new frame content as soon as it is inserted. Pro does not yet unmount the replaced frame's components on frame navigation; their React roots are released on the next full Turbo page visit.

Turbo with Auto-Bundling​

When using React on Rails' auto-bundling feature (auto_load_bundle: true) with Turbo, there's a specific ordering requirement to address:

The Challenge:

  1. Turbo's requirement: Turbo must be loaded in the <head> to avoid script re-evaluation warnings during page navigation
  2. Auto-registration's behavior: react_component with auto_load_bundle: true calls append_javascript_pack_tag during body rendering
  3. Shakapacker's requirement: All append_javascript_pack_tag calls must occur before the final javascript_pack_tag

This creates a conflict: the <head> (with javascript_pack_tag) renders before the <body> (where react_component triggers auto-appends).

The Solution: content_for :body_content Pattern

Use content_for to render your body content first, capturing auto-appends before the head renders:

<%# Step 1: Capture body content FIRST - this triggers all auto-appends %>
<% content_for :body_content do %>
<%= react_component "NavigationBarApp", prerender: true %>

<div class="container">
<%= yield %>
</div>

<%= react_component "Footer", prerender: true %>
<%= redux_store_hydration_data %>
<% end %>
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>

<%# Optional: Explicitly load non-React packs (Stimulus, shared stores, etc.) %>
<%# React component bundles are already auto-appended by react_component calls above %>
<%# Do NOT manually append component bundles here - they're already included %>
<%= append_stylesheet_pack_tag('stimulus-bundle') %>
<%= append_javascript_pack_tag('stimulus-bundle') %>
<%= append_javascript_pack_tag('stores-registration') %>

<%# Step 2: Pack tags now include all component bundles from auto-appends above %>
<%= stylesheet_pack_tag(media: 'all') %>
<%= javascript_pack_tag(defer: true) %>
</head>
<body>
<%# Step 3: Output the captured body content %>
<%= yield :body_content %>
</body>
</html>

Why This Works:

  1. Rails processes the content_for block first, which executes all react_component calls
  2. Each react_component with auto_load_bundle: true triggers append_javascript_pack_tag
  3. When the <head> renders, javascript_pack_tag includes all accumulated appends
  4. Turbo loads early in <head>, satisfying its requirement
  5. Component bundles load in the correct order

Note: While defining body content before <!DOCTYPE html> may look unusual, Rails processes content_for blocks during template evaluation, not document output order. The final HTML is correctly structured.

Common Pitfalls:

  • ❌ Don't place javascript_pack_tag before content_for blocks that call react_component
  • ❌ Don't manually append component bundles if using auto_load_bundle: true
  • ✅ Do let react_component handle bundle appending automatically

Additional Resources:

Turbo Streams (Requires React on Rails Pro)​

⚡️ React on Rails Pro Feature

Turbo Streams rely on Pro's early hydration behavior, which hydrates components as soon as they are inserted into the DOM. This is a React on Rails Pro licensed feature that is always on for Pro users.

Why Turbo Streams Need Special Handling:

Unlike Turbo Frame navigation, Turbo Streams don't dispatch the render events that React on Rails uses to hydrate components. Instead, they directly manipulate the DOM. React on Rails Pro hydrates components immediately when they are inserted into the DOM, without waiting for page load events, so Turbo Stream updates work out of the box.

Example: Create a Turbo Stream Response

<%# app/views/items/index.html.erb - Initial page with frame %>
<%= turbo_frame_tag 'item-list' do %>
<%= button_to "Load Items", items_path, method: :post %>
<% end %>
<%# app/views/items/create.turbo_stream.erb - Turbo Stream response %>
<%= turbo_stream.update 'item-list' do %>
<%= react_component("ItemList", props: @items) %>
<% end %>

What Happens:

  1. User clicks "Load Items" button
  2. Rails responds with create.turbo_stream.erb
  3. Turbo Stream updates the item-list frame with the new React component
  4. React on Rails Pro hydrates the inserted component immediately

Learn More:

  • See Streaming Server Rendering for another Pro use case
  • Working example in codebase: react_on_rails/spec/dummy/app/views/pages/turbo_stream_send_hello_world.turbo_stream.erb
  • See pro.reactonrails.com for React on Rails Pro licensing

The following documentation covers older Turbolinks versions (2.x and 5.x). While still supported by React on Rails, we recommend migrating to Turbo when possible.

React on Rails currently supports:

  • Turbolinks 5.x (e.g., 5.0.0+) - Auto-detected
  • Turbolinks 2.x (Classic) - Auto-detected
  • See Turbolinks on Github

You may include Turbolinks either via npm/yarn/pnpm (recommended) or via the gem.

As you switch between Rails HTML controller requests, you will only load the HTML and you will not reload JavaScript and stylesheets. This definitely can make an app perform better, even if the JavaScript and stylesheets are cached by the browser, as they will still require parsing.

  1. Either avoid using React Router or be prepared to deal with any conflicts between it and Turbolinks.
  2. Use one JS and one CSS file throughout your app. Otherwise, you will have to figure out how best to handle multiple JS and CSS files throughout the app given Turbolinks.
  1. React Router handles the back and forward buttons, as does Turbolinks. You might be able to make this work. Please share your findings.
  2. You want to do code splitting to minimize the JavaScript loaded.

Installation​

Install Checklist​

  1. Include turbolinks via your package manager as shown in the react-webpack-rails-tutorial or include the gem "turbolinks".
  2. Included the proper "track" tags when you include the javascript and stylesheet:

For Turbolinks 5.x:

  <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>

For Turbolinks 2.x (Classic):

  <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>

Note: If you're using modern Turbo (recommended), use 'data-turbo-track' => 'reload' instead of 'data-turbolinks-track'. See the "Using Turbo" section at the top of this document.

  1. Add turbolinks to your application.js file:
    //= require turbolinks

See the instructions on installing from NPM.

import Turbolinks from 'turbolinks';
Turbolinks.start();
Async script loading​

Async script loading can be done like this (starting with Shakapacker 8.2):

  <%= javascript_include_tag 'application', async: Rails.env.production? %>

If you use document.addEventListener("turbolinks:load", function() {...}); somewhere in your code, you will notice that Turbolinks 5 does not fire turbolinks:load on initial page load. A quick workaround for React on Rails earlier than 15 is to use defer instead of async:

  <%= javascript_include_tag 'application', defer: Rails.env.production? %>

More information on this issue can be found here: https://github.com/turbolinks/turbolinks/issues/28

When loading your scripts asynchronously your components may not be registered correctly. Call ReactOnRails.reactOnRailsPageLoaded() to re-initialize like so:

document.addEventListener('turbolinks:load', function () {
ReactOnRails.reactOnRailsPageLoaded();
});

React on Rails 15 fixes both issues, so if you still have the listener it can be removed (and should be as reactOnRailsPageLoaded() is now async).

warning

Async Scripts with Turbolinks Require Pro

If you use async script loading with Turbolinks, you need Pro's early hydration behavior to prevent race conditions. This is a React on Rails Pro licensed feature.

Without Pro's early hydration, async scripts may not be ready when Turbolinks fires navigation events, causing components to fail hydration.

Alternatives:

  • Use defer instead of async (waits for full page load before hydration)
  • Upgrade to modern Turbo (recommended)
  • Use React on Rails Pro (early hydration is automatic for Pro users)

React on Rails will automatically detect which version of Turbolinks you are using (2.x or 5.x) and use the correct event handlers.

For more information on Turbolinks 5: https://github.com/turbolinks/turbolinks

Technical Details and Troubleshooting​

CSRF and MIME Type Handling​

  • CSRF tokens: Turbolinks 5 changes the head element by JavaScript (not only body) on page changes with the correct csrf meta tag. Be thorough checking CSRF tokens, especially when multiple windows are opened, as the CSRF helper in ReactOnRails needs to work with Turbolinks5.
  • MIME type handling: Turbolinks 5 sends requests with Accept: text/html only (not Accept: */*), which makes Rails behave differently compared to normal requests. For more details on the special handling of */* you can read Mime Type Resolution in Rails.
  • Multiple Webpack bundles: If you're using multiple Webpack bundles, make sure that there are no name conflicts between JS objects or Redux store paths.

To turn on tracing of Turbolinks events, put this in your registration file, where you register your components.

ReactOnRails.setOptions({
traceTurbolinks: true,
turbo: true,
});

Rather than setting the value to true, you could set it to TRACE_TURBOLINKS, and then you could place this in your webpack.client.base.config.js:

Define this const at the top of the file:

const devBuild = process.env.NODE_ENV !== 'production';

Add this DefinePlugin option:

  plugins: [
new webpack.DefinePlugin({
TRACE_TURBOLINKS: devBuild,
}),

At Webpack compile time, the value of devBuild is inserted into your file.

Once you do that, you'll see messages prefixed with TURBO: like this in the browser console:

Turbolinks Classic:

TURBO: WITH TURBOLINKS: document page:before-unload and page:change handlers installed. (program)
TURBO: reactOnRailsPageLoaded

Turbolinks 5:

TURBO: WITH TURBOLINKS 5: document turbolinks:before-render and turbolinks:render handlers installed. (program)
TURBO: reactOnRailsPageLoaded

We've noticed that Turbolinks doesn't work if you use the RubyGem versions of jQuery and jQuery ujs. Therefore, we recommend using the JS packages instead. See the tutorial app for how to accomplish this.

Show we only install the Turbolinks handlers once