Search code examples
javascriptruby-on-railsturbolinksactioncableturbolinks-5

Rails Action Cable and Turbolinks: avoid multiple bindings


I have a code in my application.html.haml that decides whether to subscribe the user to a given channel or not, depending on some user attribute.

The thing is that, given I have this piece of code in the body, when I click a link to be redirected to another page, it subscribes the user again instead of keeping the old connection, so I have the receive() method being executed multiple times.

This is my code in application.html.haml:

%html
  %head
    -# other stuff
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  %body
    -# other stuff

    - if current_user.is_admin?
      :coffee
        MyApp.AdminNotifications.init()

assets/javascripts/notifications/admin_notifications.js.coffee

window.MyApp.AdminNotifications = class AdminNotifications
  @init: ->
    App.cable.subscriptions.create "ProductsChannel",
      received: (data) ->
        console.log 'Data Received' # This is being executed multiple times

The first time the page is loaded, when I broadcast a message to that channel, the console is logging "Data Received" just once. I click a link to be redirected to another page, I broadcast a new message to that channel, and now the console is logging "Data Received" twice. And so far so on...

In fact, when I run on the chrome's console:

App.cable.subscriptions.subscriptions

It returns multiple subscriptions to the same channel (once for every time I clicked on a link and got redirected to a different page).

Note: There is more action cable setup that I am not adding to this post because it's working fine.

How can I avoid that? Any ideas?


Solution

  • It looks like you only want a single instance of MyApp.AdminNotifications, so you may only need to add a class attribute to flag that the class has been initialized:

    window.MyApp.AdminNotifications = class AdminNotifications
      @init: ->
        unless @initialized
          App.cable.subscriptions.create "ProductsChannel",
            received: (data) ->
              console.log 'Data Received'
          @initialized = true
    

    In the future you may want to wrap the Action Cable API to manage your own subscriptions.


    As a general rule when working with Turbolinks, it is preferable to include as much stuff as possible in your application.js file rather than in inline scripts (see: https://github.com/rails/rails/pull/23012#issue-125970208). I'm guessing you used an inline script so you could conditionally run it (if current_user.is_admin?). A popular approach is to use meta tags to convey this data, so in your head:

    <meta name="current-user-is-admin" content="true">
    

    Then you could have:

    window.MyApp.User = class User
      constructor: ->
        @isAdmin = @getMeta('current-user-is-admin') == 'true'
    
      getMeta: (name) ->
        meta = document.querySelector("meta[name=#{name}]")
        meta.getAttribute('content') if meta
    

    And finally, to remove the AdminNotifications init code from the layout include this somewhere in your application.js:

    document.addEventListener('turbolinks:load', ->
      window.MyApp.currentUser = new window.MyApp.User
      window.MyApp.AdminNotifications.init() if window.MyApp.currentUser.isAdmin
    )
    

    Hope that helps!