Updating and Deleting from Temporary Assigns in Phoenix LiveView

August 07, 2020

Tags: Elixir, Phoenix, LiveView

Phoenix LiveView is a great library for building real-time, reactive apps that are rendered server-side. We've used it in both large-scale, complex apps and simpler admin dashboards in order to build rich user experiences without the overhead of bootstrapping a modern-day frontend framework.

One of the many great aspects of LiveView is the focus library maintainers have put on optimizing and minimzing data sent "over the wire" (in this case, over the WebSocket connection). This means faster and more fluid experiences for your users — plus saved bandwidth and memory allocation.

Temporary assigns is one of the optimization techniques the library makes available to developers. Assigns are stateful in a LiveView's backing process on the server. When you mark an assign as temporary, the process resets the value after rendering, freeing up some memory.

For example:

# posts_live.ex
def mount(_params, _session, socket) do
  socket = assign(socket, :posts, Posts.list_posts())
  {:ok, socket, temporary_assigns: [posts: []]}
end

After the initial render, the value of posts in socket.assigns will be reset to [] instead of the much larger list of all %Post{} structs.

In addition to freeing up memory, this technique optimizes the diff; since the process now has an empty list, when you add or update posts it'll only re-render what's new/changed instead of the entire list.

But, in that case, how does LiveView know how to render the new post among the HTML for the posts already rendered? This is where phx-update comes into play.

Let's say you have a form component that saves a new post:

# post_form_component.ex
def handle_event("save", %{"post" => post_params}, socket) do
  case Post.create_post(post_params) do
    {:ok, post} ->
      send(self(), {:updated_post, post})

      {:noreply,
       socket
       |> put_flash(:info, "Post created successfully")
       |> push_patch(to: Routes.post_path(socket, :index))}

    {:error, changeset} ->
      {:noreply, assign(socket, :changeset, changeset)}
  end
end

(Note: this is using a stateful component; you'll want to make sure you're using phx-target. If you're not using a stateful component, you don't need the send/2 step above.)

# posts_live.ex
def handle_info({:updated_post, post}, socket) do
  socket =
    socket
    |> update(:posts, fn posts -> [post | posts] end)

  {:noreply, socket}
end
<!-- posts_live.html.eex -->
<div id="posts" phx-update="prepend">
  <%= for post <- @posts do %>
  <p id="<%= post.id %>"><%= post.title %></p>
  <% end %>
</div>

You have a few options for phx-update. If you use either prepend and append, LiveView will just insert the newly rendered post either at the beginning or end of the container (in this case, div#posts).

How about updates? Let's say you update the title of a post. LiveView operates on DOM ids, so it knows when you try to render a post with the same id. Instead of appending or prepending, it replaces the existing element entirely. In this case, you'd get an updated p containing the updated post title.

But how about deletions? What if we want to remove a post from the list? Using the same principles, we can just hide the post.

# post_form_component.ex
def handle_event("delete", _params, socket) do
  case Posts.delete_post(socket.assigns.post) do
    {:ok, post} ->
      send(self(), {:deleted_post, post})

      {:noreply,
       socket
       |> put_flash(:info, "Post deleted successfully")
       |> push_patch(to: Routes.posts_path(socket, :index))}

    {:error, changeset} ->
      {:noreply, assign(socket, :changeset, changeset)}
  end
end
# posts_live.ex
def handle_info({:deleted_post, post}, socket) do
  socket =
    socket
    |> update(:posts, fn posts -> [post | posts] end)

  {:noreply, socket}
end
<!-- posts_live.html.eex -->
<div id="posts" phx-update="prepend">
  <%= for post <- @posts do %>
    <p id="<%= post.id %>"
        <%= if post.__meta__.state == :deleted do %>class="hidden"<% end %>>
      <%= post.title %>
    </p>
  <% end %>
</div>

(Note: Ecto.Schema.Metadata sets the state to :deleted here automatically, but you could also use a virtual attribute on the schema itself.)

Now, when LiveView re-renders, it knows an update happened to the post because of the DOM id and will add the hidden class, which will remove the element from the DOM (using display: none;).

Using temporary assigns opens up a whole world of powerful state updates that are also memory efficient. Using these techniques you can build a powerful UI to do all sorts of data management on large lists without having to navigate from the LiveView or re-render an entire list in the HTML.

Logan Leger

Founder & CEO

Founder. Engineer and entrepreneur. Husband and father. Writes in Ruby, Elixir, JavaScript, and Swift.

let's talk.

Together we can make something great.

contact us about new work

Contact

225-407-4520

hello@newaperio.com

Visiting Address

640 Main Street, Suite B

Baton Rouge, Louisiana 70801

Social Media