turbolinks:load fires twice

Created on 22 May 2016  ·  31Comments  ·  Source: turbolinks/turbolinks

I am experiencing the turbolinks:load event being fired twice on initial load of the page as well as each subsequent request for more content from the backend.

These are the versions I'm using:

  • gem 'rails', '>= 5.0.0.rc1', '< 5.1'
  • gem 'turbolinks', '~> 5.x'

I subscribe to this event once in my client side: $(document).on("turbolinks:load", function () { .... I was unable to find anything in the documentation nor on Google search results about this event being fired more than once, so I'm not sure how to proceed fixing this. Does anyone have any methods they could recommend in discovering the cause to this?

Also, in the call stack below, does it show jQuery invoking the turbolinks:load after Turbolinks itself invokes that event?
screenshot from 2016-05-21 23-06-32

Most helpful comment

I fixed this problem by making my event listeners idempotent, e.g. I changed the selector for my jQuery on event listener from document to a more unique element on my page (however, one which was still the parent of the event target).

More simply put, I fixed this by changing this:

$(document).on('click', '.my-elements-class-name', function(event) {
...
});

to this:

$('.primary-container').on('click', '.my-elements-class-name', function(event) {
...
});

You don't need to disable turbolinks cache or turbolinks previews for this to work.

Thank you @nateberkopec for the knowledge you shared here which enabled me to find this fix!

All 31 comments

What version of Turbolinks are you running? (post output of $ bundle list | grep turbolinks)

No, that doesn't show JQuery firing turbolinks:load - JQuery overrides a lot of event handling stuff and inserts itself in. Sort of like ActiveSupport.

$ bundle list | grep turbolinks
  * turbolinks (5.0.0.beta2)
  * turbolinks-source (5.0.0.beta4)

Could this be the same issue as https://github.com/turbolinks/turbolinks/issues/81#issuecomment-215127019?

It might be related? However, I'm using Rails and Turbolinks is coming through the gem in my Gemfile, so it is being put in my precompiled assets. And this script tag does appear in my <head> tag.

If your <script> is in <head>, it’s not the same issue.

Could you try to distill this down to a reproducible test case for us?

I'd be happy to. I have not done that before, is there a template or something I can go off of or a general process one goes through to do that? Or is it just a matter of creating as simple of an app as possible that replicates the issue?

Or is it just a matter of creating as simple of an app as possible that replicates the issue?

Bingo 😁 No template at this time, unfortunately. But see if you can reproduce the issue by making a small test app.

I'll do my best 😀

Well this is beyond frustrating...I've attempted this and I can't get the application (which is just the result of running the rails new turbolinks-issue _5.0.0.rc1 --database=postgresql command, pasting in the Gemfile I had (not a ton added), running the devise and figaro installers, and at no point could I get my application.js file to be served. I've never run across this before.

In my Gemfile, I have the following:

source 'https://rubygems.org'


gem 'rails', '>= 5.0.0.rc1', '< 5.1'
gem 'pg', '~> 0.18'
gem 'puma', '~> 3.0'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'

gem 'jquery-rails'
gem 'turbolinks', '~> 5.x'
gem 'jbuilder', '~> 2.0'

gem 'jt-rails-address', '~> 1.0'
gem 'devise', github: 'plataformatec/devise'
gem 'faker' # TODO: Put this in development as soon as you are done faking data in production.
gem 'omniauth'
gem 'figaro'

group :development, :test do
  gem 'byebug', platform: :mri
end

group :development do
  gem 'web-console'
  gem 'listen', '~> 3.0.5'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
  gem 'rack-mini-profiler'
end

gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

In my app/views/layouts/application.html, I have the following:

<!DOCTYPE html>
<html>
  <head>
    <title>TurbolinksIssue</title>
    <%= csrf_meta_tags %>

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

  <body>
    <%= yield %>
  </body>
</html>

In my app/assets/javascripts/application.js file I have the following:

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require hello
//= require turbolinks
//= require_tree .
var i = 0;
$(document).on("turbolinks:load", function () {
console.log(i++);
}

Does anyone know why I can't get any of my javascript assets to load? I put the //= require hello in there after I add the same turbolinks:load handler to a separate file called hello.js as well and none of my javascript assets will be placed into the <head> of my application I'm trying to replicate the issue in. Do I need to do anything else to get the assets served? I even created a scaffold: rails g scaffold people first_name last_name, ran the migrations and still no assets being served.

Before I was able to get the above test app to work, we figured out what was going on...kinda. There is a file in our javascript assets called init.js. init.js subscribes to the turbolinks:load event. This file was being invoked more than once, so the event was being fired twice, it was being handled twice. Changing the name of the file to app-init.js stopped the double invocation I was seeing. I'm sorry for the waste of time!

No problem! Glad you figured it out.

Hello @jakehockey10, I hope your doing well.

I am experiencing the same frustrating issue that you seemed to. Any event listeners added within

document.addEventListener("turbolinks:load", function(){...});

Are added again every time the "turbolinks:load" event fires. ex, if I click a link, then click second link, the turbolinks:load event has fired 3 times, and any .on('click') events will fire 3 times. (extremely frustrating)

I see you were able to solve your issue, but my question for you is: what do you mean by

init.js subscribes to the turbolinks:load event.

Any help is much appreciated.

@eozelius what I meant by that is that I had a javascript asset named init.js in my assets pipeline that was subscribing to the "turbolinks:load" event more than once. By my subscription looks like this, instead:

$(document).on("turbolinks:load", function () { // subscription to the "turbolinks:load" event
    ....
});

@jakehockey10 thanks for the reply.

I see, I had the same issue since I was using $(document).on("turbolinks:load", function () {...}); in several of my js files, which as mentioned above will cause any events defined within that to fire as many times as the turbolinks:load event has been fired.

The solution that I will implement is to not use the turbolinks:load and instead use delegation. After rereading the turbolinks documentation several times I found this:

When possible, avoid using the turbolinks:load event to add event listeners directly to elements on the page body. Instead, consider using event delegation to register event listeners once on document or window.

Cheers.

@eozelius

Oh my, I missed that in the documentation! I'm not even familiar with that concept yet. Do you have any links you can send me that helped you understand that concept?

Yeah, I only found out delegation existed after 3 years of professional jquery development hahaha.

Essentially delegation if you want to add an click listener to a <li>, you add the add the .on('click') listener to the <ul>, and then pass a 'li' selector.

$('ul').on('click', 'li', function(){...})

so when the <ul> is clicked, jQuery will try to find a <li> within the <ul>, and if it can find that <li> it will fire the function.

disclaimer: I have only been working with delegation for a few hours, so I could be mistaken on some concepts.

Documentation from http://api.jquery.com/on/

.on( events [, selector ] [, data ], handler )
selector
Type: String
A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element.

another great explanation: https://learn.jquery.com/events/event-delegation/

@eozelius Thanks, I really appreciate you sharing with me this information. Good luck with what you are working on!

no problem dude @jakehockey10. happy coding.

@jakehockey10 One last peculiar annoying thing about using delegations and turbolinks together.

All your event listeners have to be added to the $(document) or $(window) objects, because everything else even <html> gets reloaded, and the events won't fire on them. for example:

// Works
$(document).ready(function(){
$(document).on('click', 'ul li', function(){ ... });
});

// Does Not work
$(document).ready(function(){
$('ul').on('click', 'li', function(){ .... });
});

Hey @eozelius - no need to wrap in a $(document).ready when you're using the event delegation pattern on document. You use the ready callback when the element you're binding to might not exist yet. Not an issue for document – it will exist.

Hello @packagethief, thanks for the info.

I used the $(document).ready only because I am still quite new to delegation, and I am used to added event listeners only after the page has loaded.

But yes, when using turbolinks it is unnecessary.

Hi there,
I'm having a similar issue. After the second click, my assets are loaded twice (see picture : https://postimg.org/image/fuest7qjx/ ).
Any solution if I don't want to use delegations ?

Here's a working solution / hack :-)

Remove the event handler just before implementing it again like this:

$(document).on('turbolinks:load', function () {

    // unregister the event listener
    $('div#target').off('click');

    // register the event listener
    $('div#target').on('click', function (e) {
        // do whatever..
    });

});

This issue is not resolved. Why do you close it?

Hi Guys,
I'm using react_on_rails with turbolinks, my issue was app was loading twice after my second redirection in the app. Issue of duplicates got fixed with the following code, I'm going ahead with this as a hot fix for now. I might be completely wrong, please let me know if there is any issues in this approach.

// ../HelloWorld/startup/registration.jsx
document.addEventListener("turbolinks:before-visit", function() {
  Turbolinks.clearCache();
})

@RathanKumar actually, using this hotfix you just have disabled all the caching features of Turbolinks (so no fast back-buttons, no pages previews and etc). But... you know, more I deal with Turbolinks, more I want to just remove it at all from my project. Always I have to break my brains trying to adapt my or third-party javascript so it will work fine with Turbolink cache, and almost always you have to find new ways of workarounds, and almost always the bugs is not obvious from first application run. It is easier to just make normal SPA application with React or Angular.

Same here.
This worked for me.

import $ from 'jquery'
import Turbolinks from 'turbolinks'
Turbolinks.start()

document.addEventListener("turbolinks:before-render", function() {
  Turbolinks.clearCache()
})


$(() => {
  $('select').select2({
    placeholder: "Por favor selecione",
    width: '100%',
    allowClear: true
  })
})

For me sometimes the event turbolinks:before-visit is triggered tons of time when I click a link, I don't know why this happens, because at first everything works fine, but somehow when you navigate between pages and you do some actions then you go to that page where the event is, the bug starts ! sometimes no bug at all ! so I have no idea what's the reason behind

@jimchild49 likely you have somewhere in your js-code some bindings for this action ($(document).on('turbolinks:before-visit')) in place which is reevaluated every time you make some action on page or some navigation. So you bind same function multiple times for this hook (turbolinks:before-visit).

I fixed this problem by making my event listeners idempotent, e.g. I changed the selector for my jQuery on event listener from document to a more unique element on my page (however, one which was still the parent of the event target).

More simply put, I fixed this by changing this:

$(document).on('click', '.my-elements-class-name', function(event) {
...
});

to this:

$('.primary-container').on('click', '.my-elements-class-name', function(event) {
...
});

You don't need to disable turbolinks cache or turbolinks previews for this to work.

Thank you @nateberkopec for the knowledge you shared here which enabled me to find this fix!

I don't know if anyone else here spoke to this point as well but...

If Turbolinks is loaded more than once into a single layout, it'll call fire the turbolinks:load listener for each Turbolinks load.

For example, let's say in your layout you have 2 js manifest files and each one includes turbolinks. But you only have one block $(document).on("turbolinks:load", function(){})...etc

ALSO, a gotcha to watch out for with the event delegation strategy...

If you're listening on document for clicks... be careful not to add too many listeners. After just a few taps of the document you'll have a very slow or even a crashing browser!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

coffeebite picture coffeebite  ·  29Comments

nerdcave picture nerdcave  ·  16Comments

jaredgalanis picture jaredgalanis  ·  23Comments

wayneashleyberry picture wayneashleyberry  ·  16Comments

kstratis picture kstratis  ·  13Comments