Stimulus: Controllers disconnect method fired after Turbolinks `before-cache` method

Created on 13 Feb 2018  路  3Comments  路  Source: hotwired/stimulus

I have a Stimulus controller used to create a D3 svg in the connect method. If I do no cleanup of the DOM, navigating away from the page and back (using the back button) causes the connect method to be fired again and a second svg to be written into the DOM. This makes sense to me since I expected Turbolinks to have cached the first svg but my Stimulus controller method assumes there is nothing there yet.

To correct this problem, I attempted to delete the svg in the Stimulus disconnect method. I expected that removing the DOM here would ensure that Turbolinks would not to cache it. However, both the turbolinks:before-cache and turbolinks:load events are fired _before_ the disconnect method is executed in my controller. This is preventing my DOM cleanup from being cached correctly by Turoblinks.

Most helpful comment

For posterity, another approach is to bind to the turbolinks event and in your handler manually execute whatever lifecycle events you need. Currently testing this approach, as we have a lot of modules that lean on the Turbolinks before-cache event for DOM cleanup, timeout clearing, etc:

import { Application } from "stimulus";
const application = Application.start();

document.addEventListener('turbolinks:before-cache', function() {
  application.controllers.forEach(function(controller){
    if(typeof controller.teardown === 'function') {
      controller.teardown();
    }
  });
});

export default application;

All 3 comments

To correct this problem, I attempted to delete the svg in the Stimulus disconnect method. I expected that removing the DOM here would ensure that Turbolinks would not to cache it. However, both the turbolinks:before-cache and turbolinks:load events are fired _before_ the disconnect method is executed in my controller.

Everything鈥檚 working as designed here, but I agree the timing is a little confusing.

Turbolinks fires its before-cache event just before it replaces document.body, and the load event just afterwards.

Stimulus uses the DOM MutationObserver API to detect when elements enter and leave the document. Mutation observers watch for changes and queue a microtask to notify JavaScript of the batched changes.

So, the actual disconnection of the element from the document happens during the document.body replacement, between the before-cache and load events. The load event fires synchronously afterwards, and the mutation observer callback happens after that at the end of the event loop.

It does seem natural to expect that disconnect() would be the place to tear down changes to the DOM for Turbolinks caching, but that callback fires after the element is disconnected, and Turbolinks caches the old body before replacing it.

Here are my recommendations for you:

  1. If you really are just rendering an SVG and don鈥檛 need any of D3鈥檚 interactive functionality, try checking to see whether the image is present in the DOM before rendering.

     connect() {
       if (/* this.element does not have an svg child yet */) {
         /* render with D3 */
       }
     }
    
  2. Otherwise, explicitly listen for the turbolinks:before-cache event to tear down D3 by adding a data-action to the controller element:

    <div data-controller="svg" data-action="turbolinks:before-cache@window->svg#teardown">
    

Thanks for the explanation and suggestions. It's unfortunate that I can't fully "own" the DOM in the Stimulus controller in a way that works well with Turbolinks but I'm guessing you don't want to be dependent on Turbolinks lifecycle events. The data-action suggestion was new to me - any more reading on that syntax?

Thanks!

For posterity, another approach is to bind to the turbolinks event and in your handler manually execute whatever lifecycle events you need. Currently testing this approach, as we have a lot of modules that lean on the Turbolinks before-cache event for DOM cleanup, timeout clearing, etc:

import { Application } from "stimulus";
const application = Application.start();

document.addEventListener('turbolinks:before-cache', function() {
  application.controllers.forEach(function(controller){
    if(typeof controller.teardown === 'function') {
      controller.teardown();
    }
  });
});

export default application;
Was this page helpful?
0 / 5 - 0 ratings

Related issues

tiagocassio picture tiagocassio  路  3Comments

savroff picture savroff  路  4Comments

thiagomajesk picture thiagomajesk  路  4Comments

thooams picture thooams  路  4Comments

jenkijo picture jenkijo  路  3Comments