Checkbox does not render inside LiveComponent for newly created LiveComponents

I was following a simple todo list tutorial and ran into an issue when trying to convert some logic into a LiveComponent. I am trying to generate a list of Todo items (which are LiveComponents), but I can’t figure out why newly created Todo items do not render a checkbox next to them. They will only appear until after I refresh the page. Any guidance would be greatly appreciated!

The LiveView todo_live.ex:

defmodule TodoListWeb.TodoLive do
  use TodoListWeb, :live_view

  alias TodoList.Todos

  def mount(_params, _session, socket) do
    Todos.subscribe()

    {:ok, fetch(socket)}
  end

  def handle_event("add", %{"todo" => todo}, socket) do
    Todos.create_todo(todo)

    {:noreply, fetch(socket)}
  end

  def handle_event("delete", %{"id" => id}, socket) do
    todo = Todos.get_todo!(id)
    Todos.delete_todo(todo)

    {:noreply, fetch(socket)}
  end

  def handle_info({Todos, [:todo | _], _}, socket) do
    {:noreply, fetch(socket)}
  end

  def handle_info({:update, %{"id" => id, "title" => title}}, socket) do
    todo = Todos.get_todo!(id)
    Todos.update_todo(todo, %{title: title})

    {:noreply, fetch(socket)}
  end

  def handle_info({:toggle_completed, %{"id" => id}}, socket) do
    todo = Todos.get_todo!(id)
    Todos.update_todo(todo, %{completed: !todo.completed})

    {:noreply, fetch(socket)}
  end


  defp fetch(socket) do
    assign(socket, todos: Todos.list_todos())
  end
end

todo_live.html.leex:

<div>
  <h1>Todo List</h1>
  <form action="#" phx-submit="add">
    <%= text_input :todo, :title, placeholder: "What do you want to do?" %>
    <%= submit "Add", phx_disable_with: "Adding..." %>
  </form>

  <%= for todo <- @todos do %>
    <%= live_component @socket, TodoListWeb.TodoComponent, id: todo.id, todo: todo %>
  <% end %>
</div>

The LiveComponent todo_component.ex:

defmodule TodoListWeb.TodoComponent do
  use TodoListWeb, :live_view
  use Phoenix.LiveComponent

  alias TodoList.Todos

  def mount(socket) do
    {:ok, assign(socket, updating?: false)}
  end

  def handle_event("pre_update", _, socket) do
    {:noreply, assign(socket, updating?: true)}
  end

  def handle_event("cancel", _, socket) do
    {:noreply, assign(socket, updating?: false)}
  end

  def handle_event("update", %{"todo" => %{"title" => title}}, socket) do
    send self(), {:update, %{"id" => socket.assigns.id, "title" => title}}

    {:noreply, assign(socket, updating?: false)}
  end

  def handle_event("toggle_completed", _, socket) do
    send self(), {:toggle_completed, %{"id" => socket.assigns.id}}

    {:noreply, socket}
  end
end

todo_component.html.leex:

<div>
  <%= checkbox(:todo, :completed, phx_click: "toggle_completed", value: @todo.completed, phx_target: @myself) %>
  <%= @todo.title %>
  <%= if @updating? do %>
    <form action="#" phx-submit="update" phx-target="<%= @myself %>">
      <%= text_input :todo, :title, value: @todo.title %>
      <%= submit "Save", phx_disable_with: "Saving..." %>
      <button type="button" phx-click="cancel" phx-target="<%= @myself %>">Cancel</button>
    </form>
  <% else %>
    <button
      phx-click="pre_update"
      phx-target="<%= @myself %>"
    >
      Update
    </button>
  <% end %>
  <button
    class="todo-delete-button"
    phx-click="delete"
    phx-value-id="<%= @todo.id %>"
  >
    Delete
  </button>
</div>

I’ve had this issue come up quite a bit, and while I can’t say for sure why it happens, I’ve found that giving the item a unique DOM id always fixes it. With a helper like checkbox/3, you can simply add the id to the opts list, like:

<%= checkbox(:todo, :completed,
      phx_click: "toggle_completed",
      value: @todo.completed,
      phx_target: @myself,
      id: "checkbox-#{@todo.id}") %>

Ah, that was it. Coming from a React background, this seems similar to providing keys to mapped React components. Thank you!