docs/oss/building-features/turbolinks.md
@hotwired/turbo), as Turbolinks becomes obsolete.Turbo is the modern replacement for Turbolinks, providing fast navigation through your Rails app without full page reloads.
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 (works out of the box)
Turbo Frames work with React components without any special configuration:
<%# 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 using React on Rails' auto-bundling feature (auto_load_bundle: true) with Turbo, there's a specific ordering requirement to address:
The Challenge:
<head> to avoid script re-evaluation warnings during page navigationreact_component with auto_load_bundle: true calls append_javascript_pack_tag during body renderingappend_javascript_pack_tag calls must occur before the final javascript_pack_tagThis 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:
content_for block first, which executes all react_component callsreact_component with auto_load_bundle: true triggers append_javascript_pack_tag<head> renders, javascript_pack_tag includes all accumulated appends<head>, satisfying its requirementNote: 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:
javascript_pack_tag before content_for blocks that call react_componentauto_load_bundle: truereact_component handle bundle appending automaticallyAdditional Resources:
⚡️ React on Rails Pro Feature
Turbo Streams require the
immediate_hydration: trueoption, which is a React on Rails Pro licensed feature.
Why Turbo Streams Need Special Handling:
Unlike Turbo Frames, Turbo Streams don't dispatch the normal turbo:render events that React on Rails uses to hydrate components. Instead, they directly manipulate the DOM. The immediate_hydration option tells React on Rails to hydrate the component immediately when it's inserted into the DOM, without waiting for page load events.
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,
immediate_hydration: true) %>
<% end %>
What Happens:
create.turbo_stream.erbitem-list frame with the new React componentimmediate_hydration: true ensures the component hydrates immediatelyLearn More:
immediate_hydration documentationreact_on_rails/spec/dummy/app/views/pages/turbo_stream_send_hello_world.turbo_stream.erbMigration Note: If you're referencing PR #1620 discussions, note that force_load was renamed to immediate_hydration in v16.0.
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:
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.
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.
application.js file:
//= require turbolinks
See the instructions on installing from NPM.
import Turbolinks from 'turbolinks';
Turbolinks.start();
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 Feature
If you use async script loading with Turbolinks, you must enable
immediate_hydration: trueto prevent race conditions. This is a React on Rails Pro feature.Without
immediate_hydration: true, async scripts may not be ready when Turbolinks fires navigation events, causing components to fail hydration.Alternatives:
- Use
deferinstead ofasync(waits for full page load before hydration)- Upgrade to modern Turbo (recommended)
- Use React on Rails Pro for
immediate_hydration: true
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
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.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.