LiveView rendering conditional elements that should be mutually exclusive

It’s not much to look at, but I have a loop rendering some cards with action buttons. These buttons are set using if/else in the template so they SHOULD be mutually exclusive, but when I click the “Notify me” button, both buttons get rendered (as pictured). The issue isn’t present on first load and doesn’t show up until I click “Notify me”. I’m using LV 0.16 but it also happened on 0.15.

Screen Shot 2021-08-12 at 8.05.31 AM

LiveView template:

<%= for act <- @acts do %>
  <div class="container items-center px-5 py-12 text-blueGray-500 lg:px-20">
    <div class="p-2 mx-auto my-6 bg-white border rounded-lg">
      <div class="flex justify-between p-6 py-2 rounded-lg">
        <p class="mb-3 text-base leading-relaxed text-blueGray-500"><%= act.name %></p>
        <%= if act.notification do %>
          <button id="cancelNotification" class="btn btn-thin btn-danger" phx-click="cancel_notification" phx-value-notification_id={act.notification.id}>
            Cancel notification
          </button>
        <% else %>
          <button id="createNotification" class="btn btn-thin btn-primary" phx-click="create_notification" phx-value-act_id={act.id}>
            Notify me
          </button>
        <% end %>
      </div>
    </div>
  </div>
<% end %>

LiveView module:

defmodule MyAppWeb.Festival.ActSelectionLive do
  use MyAppWeb, :live_view

  alias MyApp.Festivals
  alias MyApp.Notifications

  @impl true
  def mount(_params, %{"festival_id" => festival_id} = session, socket) do
    new_socket =
      socket
      |> assign_defaults(session)
      |> load_acts_for_user(festival_id)

    {:ok, new_socket}
  end

  @impl true
  def handle_event("create_notification", %{"act_id" => act_id}, socket) do
    {:ok, _notification} =
      act_id
      |> Festivals.get_act!()
      |> Notifications.create_notification_for_act_and_user(socket.assigns.current_user)

    {:noreply, load_acts_for_user(socket, socket.assigns.festival_id)}
  end

  def handle_event("cancel_notification", %{"notification_id" => notify_id}, socket) do
    {:ok, _notification} =
      notify_id
      |> Notifications.get_notification!()
      |> Notifications.delete_notification()

    {:noreply, load_acts_for_user(socket, socket.assigns.festival_id)}
  end

  defp load_acts_for_user(socket, festival_id) do
    user = socket.assigns.current_user
    acts = Festivals.list_acts_for_festival_and_user(festival_id, user.id)

    assign(socket, festival_id: festival_id, acts: acts)
  end
end

LiveHelpers file (the source of assign_defaults

defmodule MyAppWeb.Views.Helpers.LiveHelpers do
  import Phoenix.LiveView

  alias MyApp.Accounts

  @doc """
  Fetches current user details from session, if present
  """
  def assign_defaults(socket, %{"current_user_id" => user_id}) do
    assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)
  end

  def assign_defaults(socket, _session), do: socket
end

Solved thanks to some help on the Discord server!

I added the button IDs to help with LV testing. Turns out that is what added the duplicate buttons. By updating the IDs to {"createNotification_#{act.id}"} the issue has been resolved