How to ensure load order with Turbo and Stimulus?

I’m in the middle of converting a Rails 6 app that didn’t use Turbolinks to Rails 7 using both Hotwire and Turbo. For context, I have a link that goes to the Rails controller’s new action, and then redirects to an edit action.

On the page that the user is redirected to, I have a div with an attached WizardController:

<div data-controller="wizard">...</div>

The WizardController.connect method depends on the baseURI of the document, as I’m testing to see if the URI string contains “new”, e.g.:

if (document.baseURI.match("/new"))
  return

Before Turbo, all this worked fine, as the baseURI of the document had already changed from new to edit. After switching to Turbo, though, it’s not working. The problem is that by the time the WizardController connects, Turbo has not changed the current baseURI from new to edit. So it always finds “new” in the string, exiting the function when not intended.

I’ve gone through a few different possibilities to make this work:

Attempt #1: Using setTimeout in the WizardController.connect to run a different method with the same code that connect previously contained:

connect() {
    setTimeout(() => {
      this.setup()
    }, 500)
  }

It works, because it forces the code run after Turbo has changed the baseURI, but just feels like a hack, plus it by definition makes the page less performant.

Attempt #2. Removing the controller from the div, and attaching it after turbo:load, which allows me to use the Stimulus connect method as expected.

<div data-wizard="true" data-controller="">...</div>
document.addEventListener("turbo:load", () => {
  let wizardYield = document.querySelector("[data-wizard='true']")
  if (wizardYield)
    wizardYield.dataset.controller = "wizard"
})

This works, but doesn’t “feel right” and makes it hard to easily understand what’s going on. I don’t like having a separate document event listener for the sole purpose of connecting this controller just to make sure Turbo’s loaded.

Attempt #3. Adding two actions to the wizard’s element and using a custom method rather than connect:

<div data-controller="wizard" action="turbo:load@document->wizard#setup load@window->wizard#setup">...</div>

This also works, and is my favorite of the three. But I’m still not wild about it, because it requires using a custom method in the Stimulus controller rather than the standard connect. And I don’t love having that separate load@window action. But without it, the setup method is only run on a Turbo visit, and is not run when the user does a manual page refresh. (I don’t understand why that’s the case - isn’t turbo:load run on a page refresh?)

Is there a ‘better’ or ‘cleaner’ way to do this? It seems like a complicated solution to something that has to have come up before. Surely others have needed to ensure that turbo:load has already run by the time Stimulus connect is being executed?

1 Like
  • Are your stimulus controllers preloaded or lazy-loaded?
  • Curious as to why you use both turbo:load and load events?
  • What does the wizard setup do?

Option 3 is a good approach, and creating a custom method to run a setup of the page on an event is a good scenario of how to use stimulus. If concerned about the name and your connect method is idempotent, then just call connect again?

turbo:load@document->wizard#connect

1 Like

The controllers are not lazy-loaded, just using the standard “out of the box” Stimulus functionality.

I have to use load@window in order to get the setup method to run if the user manually refreshes the page. It won’t run otherwise, or at least I couldn’t figure out how to make it run otherwise.

The wizard setup method does a number of things like setting up form validation and auto-submit.

Thanks for the suggestion on the naming, will give it some thought.

From documentation:

  • turbo:load fires once after the initial page load, and again after every Turbo visit. Access visit timing metrics with the event.detail.timing object.

You shouldn’t need to use load@window. Try changing turbo:load@document to turbo:load@window

I shouldn’t have to, but I do. Making the change you suggested didn’t change the result - the setup method isn’t executed unless I have the load@window action specified in addition to the turbo:load@document.

@floydj Thank you for sharing option #3. This helped me resolve a display issue with the embedded Stripe custom Connect onboarding element.

I used a Stimulus controller to load & append the onboarding element to the DOM, but for some reason the element rendered with ‘style=display: none;’ included by default. Adding ‘load@window’ and ‘turbo:load@document’ actions to the element’s container DIV and creating a Stimulus method to control the element’s display property solved this issue for me.