phoenix_live_view icon indicating copy to clipboard operation
phoenix_live_view copied to clipboard

Live components getting duplicated, page has reused element ids

Open spiffytech opened this issue 3 years ago • 0 comments

Environment

  • Elixir version (elixir -v): Elixir 1.12.3 (compiled with Erlang/OTP 24)
  • Phoenix version (mix deps): 1.6.2
  • Phoenix LiveView version (mix deps): 0.17.5
  • Operating system: Linux
  • Browsers you attempted to reproduce this bug on (the more the merrier): Firefox, Brave (each on desktop and Android)
  • Does the problem persist after removing "assets/node_modules" and trying again? Yes/no: Yes

Actual behavior

I'm seeing this sporadically. I've failed at narrowing it down to a minimal or even reliable reproduction with my limited understanding of LiveView internals. I'd appreciate guidance on reproducing so I can improve this bug report.

I sometimes see an element in my app get duplicated and inserted at the wrong location. It looks like Phoenix reuses an element id for a live component. Troubleshooting is a pain, because if I refresh the page everything renders fine. But if I interact with the corrupted page, the artifact remains.

I mostly notice this when I know my browser was disconnected from the server for a while. E.g., I put my laptop to sleep for a while, then open it again, and when I flip back to my app sometimes this happens. I don't see it when I'm actively using the app. But I can't reproduce it on command, and even this "pattern" is far from reliable.

In the latest occurrence, here's the DOM, where we can see two nodes both have the ID phx-FriuNNpFOITEIQEB-8-0 - the highlighted <details>, and an <article> adjacent to it. DOM with duplicated element IDs

That highlighted <details> element is the rogue element. It's not just not supposed to be present, it's not even placed in the right level of the DOM: it should be in the list adjacent to the other <details>, not inside another <details> element. I know morphdom does funny things when ids aren't unique, so I assume that's what's happening.

The rogue element (the "markforster" header) rogue element visual

That element's correct placement is down at the bottom of the page, where it also appears. The phx-click handler works on both copies of the element, but clicking either element causes Phoenix to render changes to the correct element, not the rogue one. Good element (visual) Here's the DOM for the correct element: Good element (DOM)

I don't know if it's relevant, but I notice the correct one has data-phx-component of 7, while the rogue element has 8. That rogue element is the first child under the first expanded <details> on the page. My app uses the URL params to decide which <details> are expanded. Maybe that affects the problem?

My app's behavior

My app keeps the <details> elements collapsed, then when one is clicked LiveView fills it in. Content on the page can be changed by other devices, and touching a <details> children often reorders the <details> (groups are sorted by date). So my vague hunch is that one device gets the page 'paused' (laptop goes to sleep, mobile browser unloads page contents from memory, something like that), a second device changes the page contents, and when the first device reconnects something goes wrong that causes the old DOM to be patched with content that has a reset data-phx-component counter value.

I have no idea if that's right, and I know I'm not giving you a lot to work with. I'm happy to do more work to narrow this down, but I couldn't figure out a reproduction with what I currently know of LiveView internal behavior.

My live component calls

Here's how I'm rendering my live components. They have id values, and according to my local dev setup, they're all unique IDs, and I don't see any chance of duplicates.

Top-level live view: top-level live component

Inside that LinkGroup component: next level of component call

I can show you whatever else you want to see.

Expected behavior

No duplicated elements.

Reproduction attempts

I have a clean room Phoenix app where I tried to reproduce this from scratch, with no success.

I tried the following, using two devices: one online and making changes, and another that goes offline for 60+ seconds at a time and then comes back online.

  • A live view that changed the id of a live component every few seconds.
  • Keeping persistent state on the server, and changing that state on one device while the other was disconnected. Prepending to a list, shuffling the list, removing arbitrary elements in the list.
  • Using one level of live component, or two levels. (My app has two levels, but I don't know if that's pertinent.)
  • Storing which live components are expanded in the URL (my app does this). Adding/removing/shuffling the list data while a device is offline.

spiffytech avatar Nov 20 '21 23:11 spiffytech