Stuck with live view push_patch and cids_destroyed

I have pretty basic live view listing todos and having “new todo” with a popup. Upon submitting, the default behavior (result of phoenix generators) is calling push_redirect back to index. However, this seems to be triggering full page reload (load indicator on top, phx_leave -> phx_close -> phx_join in websocket). This is not desired behavior, not sure if it’s caused by my mistake.

So I am trying to use push_patch instead, but I am having trouble seeing the newly added todo on the index page. I tried an approach described here in docs, with temporary assigns and phx-update="append" in the template – that will write the new todo to database, but it won’t show up on screen (I did update id’s in template). Also deleting todos doesn’t have visible effect, just database deletion.

I also tried phx-update="replace" in the template, which seems to work really nicely when deleting todos, but new todos still don’t appear… In both cases I see cids_destroyed sent from client via websocket, no idea what that means and what’s causing it. But it seems to cause full page reload, or at least very slow response.

mount

def mount(%{"list_id" => list_id}, _session, socket) do
    socket = 
      socket 
      |> assign(:todos, list_todos(list_id))
      |> assign(:user, get_user(list_id))
      |> assign(:list_id, list_id)
    # clearing whichever flash message, after 3 seconds
    if connected?(socket), do: Process.send_after(self(), :clear_flash, 3000)
    # temporary assign for push_patch, 
    # not sure if needed when phx-update="replace" in the template
    # {:ok, socket, temporary_assigns: [todos: []]} 
    {:ok, socket}
  end

github link

new todo action

defp apply_action(socket, :new, %{"list_id" => list_id} = params) do
    socket
    |> assign(:page_title, "New Todo")
    # create empty Todo, pre-filled with list_id (hidden field in form)
    |> assign(:todo, %Todo{list_id: list_id})
  end

github link

form component save todo with the infamous push_patch

defp save_todo(socket, :new, todo_params) do
    case Lists.create_todo(todo_params) do
      {:ok, _todo} ->
        socket = 
          socket
          # |> put_flash(:info, "Todo created successfully")
          # add just the new todo, but not sure whether it makes sense with phx-update="replace"
          # |> assign(:todos, todo) 
          |> push_patch(to: socket.assigns.return_to)
        {:noreply, socket}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end

github link

returns to index action

defp apply_action(socket, :index, %{"list_id" => list_id} = params) do
    socket
    |> assign(:page_title, "Listing Todos")
    |> assign(:todo, nil) # clear for the form
    |> assign(:todos, list_todos(list_id)) # TODO should this be here??
  end

github link

I am completely stuck at this point, can’t find any docs for cids_destroyed that would help me fully understand. It seems like it could be something with id’s in the template, but no idea what I’m doing wrong there and how it should be done properly.

Also not exactly sure 1. where exactly to update list of all todos and assign it to socket and 2. what’s the difference between phx-update="append" (just assigning 1 new todo to todos?) and phx-update="replace" (sending all todos every time?) - especially what’s the difference between having “replace” vs having nothing at all.

Thank you very much for your help and guidance, sorry for long post!

1 Like

Hello! :wave:

I’ve forked and made a PR to your project with minimum changes to achieve what you need. The first commit runs mix format, this is why the diff is big but you can isolate and view only the second commit, sorry about that.

I’ll try to go over a few things you asked here.

You said it was triggering a full page reload after creating a TODO and indeed it was.
The FormComponent calls OrganizerWeb.TodoLive.Index.broadcast(socket.assigns.list_id, todo, :todo_change) after creating a TODO but list_id was not present in the socket, this is what was causing the LV to crash and remount. Fixed by adding list_id: @list_id, when rendering the component.

You can get more information about phx-update here. Looks like you based your code in the Chirp app, basically you pair phx-update="append" and temporary assigns so you don’t need to hold a long list that won’t be frequently updated in memory. You said you didn’t have success with this and reverted to phx-update="replace" which is the default behavior (so you don’t need to have it in the template).

The mount is responsible for fetching the TODOs so you don’t need to update the :todos assign in the socket in your apply_action. Now that the redirect works it’s updating just fine!

3 Likes

Thank you so much for this! PR is much more than I hoped for! :+1::clap: Works like charm now. I introduced some more mess by adding the pubsub, but you fixed it :+1:

Not sure whether those cids_destroyed came from, but now they’re gone.

May I ask one more question, please? Is there a situation in LV lifecycle when the client should be sending phx_leave and subsequently phx_join again? (see image) Currently, this happens after creating a new Todo. Is that caused by the push_redirect and is that intended behavior actually? The join is relatively gif, compared to other traffic.

Thank you :pray:

Those events happen because push_redirect will shutdown the current LV and the new one will be mounted, even if you redirected to the same LV.

You can replace the redirect with push_patch(to: socket.assigns.return_to). I just tested and it also works.
After you do this you can remove assign(:todos, list_todos(list_id)) from your mount.

push_patch won’t execute mount again, but will execute handle_params, which in your case calls apply_action and your code will refresh the TODOs:

  @doc """
  Listing all todos
  """
  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Todos")
    |> assign(:todo, nil)
    |> assign(:todos, list_todos(socket.assigns.list_id)) # -> This wil execute when you push_patch, refreshing your TODOs
  end

(I didn’t realize this before, in the current code you’re fetching the TODOs twice: one time in mount and again in apply_action)

1 Like

Awesome, smooth like butter now! Thank you once again.

cids_destroyed are back, still don’t know what that means, but all seems ok, so nevermind.

I knew mount won’t get called after push_patch, but just thought I need to always pull the data in the mount. Seems like I don’t, and everything works as expected now :+1:

Don’t worry about the cids_destroyed. They’re internal to the framework. Phoenix will add an ID to each component it renders and they are used to help with the diffs, that payload carries the IDs of the components that were destroyed and should be removed from the HTML.

1 Like