Shadowed assigns with AlpineJS v3 and Phoenix LiveView 0.15

We’re running into an odd problem with AlpineJS and Phoenix LiveView, whereby Alpine reacts to the previous value of an assign when it changes. For example, with the following LEEx (lifted verbatim from Patrick Thompson’s excellent blog post:

<div id="counter" x-data="{count: <%= count %>}">
  <h1>The assigns count is: <span><%= @count %></span></h1>
  <h1>The alpine count is: <span x-text="count"></span></h1>

With Alpine v2, both the assigns count and Alpine count are kept in sync whenever the assign is updated however with Alpine v3, the Alpine counter is always one update behind the assign.

I’ve illustrated the whole problem in this Loom video.

Has anyone else seen (or can reproduce) this issue? The full LiveView is as follows:

defmodule MyApp.AlpineLive do
  use Phoenix.LiveView

  @impl Phoenix.LiveView
  def render(assigns) do
    <div id="counter" class="m-8" x-data="{count: <%= @count %>}">
      <h1>The assigns count is: <span><%= @count %></span></h1>
      <h1>The alpine count is: <span x-text="count"></span></h1>
      <button class="button button-primary" phx-click="decrement"> Decrement </button>
      <button class="button button-primary" phx-click="increment"> Increment </button>

  @impl Phoenix.LiveView
  def mount(_, _, socket) do
    count = if connected?(socket), do: 5, else: 0
    {:ok, assign(socket, count: count)}

  @impl Phoenix.LiveView
  def handle_event("increment", _, socket) do
    {:noreply, update(socket, :count, &(&1 + 1))}

  def handle_event("decrement", _, socket) do
    {:noreply, update(socket, :count, &(&1 - 1))}

FWIW our app.js integrates AlpineJS with the following option on the LiveSocket (which supports both Alpine v2 and v3):

  dom: {
    onBeforeElUpdated(from, to) {
      if (!window.Alpine) return;
      if (from.nodeType !== 1) return;
      // AlpineJS v2
      if (from.__x) window.Alpine.clone(from.__x, to);
      // AlpineJS v3
      if (from._x_dataStack) window.Alpine.clone(from, to);

So we’ve identified a workaround; the x-data behaviour in Alpine v3 appears to have changed, but it can be achieved as follows:

<div id="counter" x-data="{count: 0}" x-init="count = <%= @count %>">

Thanks for showing this new dom watcher. I was not seeing any console logs so I assumed it had not changed, but infact it was this that was the source of my issues.

I’ve tried this but with no luck

I have seen that if you call Alpine start again it re initializes and shows the correct data. My thought here is this is due to the way we are calling clone or what needs to happen to correctly re initialize

Any how heres my reproducible code.

Edit: so this looks like a possible solution Alpine.initTree()

Edit Edit:
Ok so I found it :tada:

        if (from._x_dataStack) {
          window.Alpine.clone(from, to);

and you don’t need to use x-init, Also heres something odd., look at what happens if you only use window.Alpine.initTree(to) But after inspecting the _x_dataStack array I’m not sure if that’s the correct way to do this.
Also this Expose the initTree method by KevinBatdorf · Pull Request #1648 · alpinejs/alpine · GitHub


I struggled for a sometime with this issue, including using x-init to set the value without success, the only thing that has worked is the addition of window.Alpine.initTree(to) after the window.Alpine.clone(from, to) call.

This workaround would be fine if the Alpine component only contains shadowed assigns of LiveView.

But, if the Alpine component is maintaining its own states besides of the shadowed assigns of LiveView. Every time the shadowed assigns of LiveView updates, the Alpine component’s own states will be reset by window.Alpine.initTree(to).

That’s not expected.

For example:

<div x-data={"{ count: #{@count} }"}></div>                # fine
<div x-data={"{ count: #{@count}, showTip: true }"}></div> # bad

So, I prefer the workaround provided by simoncocking.