Integrate LiveView and Turbolinks lifecycles

I’m trying to implement multiple LiveViews in our Phoenix app which uses Turbolinks.
When switching pages Turbolinks replaces the page content, but LiveViews is not aware of this and are not disconnected/destroyed.

I noticed the LiveViews are piling up by logging liveSocket.roots. In the example below you see there are 2 LiveViews initially (which is correct). When navigating from there you see new instances being added where the old instances are not being removed:

This results in JavaScript errors when the LiveView is removed from the DOM but is still receiving updates:

How we initialize and connect:

import { Socket } from "phoenix";
import LiveSocket from "phoenix_live_view";

const liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: window.csrfToken }
});

// Fires once after the initial page load, and again after every Turbolinks visit
document.addEventListener("turbolinks:load", () => {
  liveSocket.connect();
});

What would be the recommended way to integrate LiveViews with Turbolinks?
Is there a way to have both lifecycles to play nice together?

3 Likes

Hey @snap, I think I got this to work.
In your app.js:

// Disconnect current socket before navigating away, otherwise it's left open after navigation:
document.addEventListener("turbo:before-visit", function() {
  window.liveSocket.disconnect()
})

// Create socket for the new visit:
document.addEventListener("turbo:load", function() {
  let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
  let liveSocket = new LiveSocket("/live", Socket, {
    params: {_csrf_token: csrfToken},
  })

  liveSocket.connect()

  // expose liveSocket on window for web console debug logs and latency simulation:
  // >> liveSocket.enableDebug()
  // >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
  // >> liveSocket.disableLatencySim()
  window.liveSocket = liveSocket
})

Partial credit: good ol’ GitHub search.

In my case this fixes the issue where LiveViews get disconnected when you navigate away from them to a different page, and then return back pulling a cached copy from turbo cache.

I’ve also tested your issue with instances left behind via liveSocket.roots and it seems to be gone as well.

If someone found other solutions, would love to see them as well.

There’s probably something interesting you could do by destroying all LiveViews with liveSocket.destroyAllViews() when navigating away (e.g. via "turbo:before-visit") and the connecting new ones on a new visit with liveSocket.connect(), but the above solution seems to work well.

Also, bonus for anyone looking to get <turbo-stream> to work with LiveView:
https://twitter.com/_fifwb/status/1346425466823921664

3 Likes

That looks very promising, thank you so much! Will check your solution asap.

Forgot to update.

After playing around with this solution for a bit, it turns out it still doesn’t integrate LiveView well enough with Turbo Hotwire.

I had to pause my transition from Rails to Phoenix, so didn’t have a chance to resolve these issues yet. Though I really would want to get Hotwire and LiveView to play nicely with each other, as it significantly speeds up development.

Sharing my findings below, in case you want to look into it as well:

With the solution above, when you navigate back, Turbo restores previous page from its cache. If there were LiveView components on the previous page, the newly-created LiveView socket is unable to re-connect/re-hydrate them, so it throws an error and does a full page reload, which is a big bummer.

I would get one of the two errors in this case:

  1. TypeError: null is not an object (evaluating 'e.main.isConnected') - thrown by phoenix_live_view/phoenix_live_view.js at e9ba0a673333f3528e81ddf28532ae9f4210b882 · phoenixframework/phoenix_live_view · GitHub

  2. WebSocket connection to 'ws://localhost:4000/live/websocket' failed: WebSocket is closed before the connection is established.

You can avoid the full page reloads and above errors if you disable the event code in turbo:before-visit, thus leaving the old sockets behind, which certainly is not ideal either. LiveView in this case will only throw no id found for phx-FogDpZBNc9AC-Aqk as the old socket tries to connect with LV components that are no longer in the DOM.

Moving forward, I see two possible solutions:

  1. Find a way for the newly-created sockets to connect with LiveView components on cached pages. This probably involves finding a way to do a proper tear-down of LV components before Turbo navigates away and caches that page, so when we navigate back to that page, a new socket can easily connect with these components. I tried tearing down with liveSocket.destroyAllViews(), but components become un-restorable.

  2. Find a way to reuse the same LiveSocket between different pages. I experimented with this solution, but didn’t get far, as LV throws the following errors and does a full-page reload when it tries to re-hydrate a cached view:

  [Log] phx-FogHDfwpTssfMwXh error: unable to join -  – {reason: "stale"}
  [Log] phx-FogHDfwpTssfMwXh destroyed: the child has been removed from the parent -  – undefined
  [Log] phx-FogHDfwpTssfMwXh join: encountered 0 consecutive reloads -  – undefined

If you make any progress here, let me know!

1 Like

Important to mention that last phoenix_live_view version I’ve tested things on was v0.15.1. There was a major 0.16.0 release since then.

2 Likes

I’ve been curious about the feasibility of combining Turbo and LiveView so one could use Turbo iOS/Android. This thread was an interesting read!

(I know about LiveView Native, but it’s very different from the Turbo iOS/Android approach. More like React Native.)

I wonder if it could work to use LiveView without Turbo JS, inside Turbo iOS/Android, by emitting/responding to similar events. Haven’t taken the time yet to explore this whatsoever.