When return {:reply, :map, socket} on a LiveView handle_event/3?

The other day, I was doing a mob programming session with some friends, working on a LiveView project. They don’t have experience with Elixir, and while we were adding the handle_event/3 with a return {:noreply, socket} , they asked why there was “no reply”? Aren’t we sending a response? The UI is updated.

I had no answer as I never had a case to use the other reply option {:reply, map(), socket}. After some research, I found a case for using it.

Let’s explore the difference between {:noreply, socket} and {:reply, map(), socket}. When writing LiveView code, a common pattern we often use is as follows:

  @impl true
  def handle_event("highlight", %{"note_id" => id}, socket) do
    note =
      id
      |> Resources.find_note()
      |> Resources.highlight_note()

    {:noreply, assign(socket, :note, note)}
  end

This is a valid code, and it’s probably what most people use.

The {:noreply, socket} means you don’t have additional information to return. But if you assign a value to the socket, isn’t the caller getting extra information? Well, yes and no. When you update the socket, LiveView knows what needs to update and re-renders the components to display the new information we set in the socket.

When you reply to a call, you send information that may not be in the socket or that the caller does not have direct access to. An example is a JS hook that can use the extra information to change the UI.

I tried to find an example of using the reply in a few projects without success, so I came up with an example that it’s not incredible, but it will show the use of it, and you may find a way to improve your codebase.

The idea is to have a JS hook highlighting a keyword in a note. When we click a button, the hook dispatches an event we will handle and reply with the word to emphasise. Returning only the word and executing the rest in the frontend will prevent us from sending much data over the socket for a visual change.

The hook is simple, and it will push an event, get a keyword as a response and replace it with <mark>keyword</mark>.

Hooks.Highlight = {
  mounted() {
    const el = document.getElementById("highlight-button");

    el.addEventListener("click", _e => {
      this.pushEvent("highlight", {
        note_id: el.dataset.note_id
      }, (reply, _ref) => {
        const content = document.getElementById("note").textContent;
        const highlighted_content = content.replace(new RegExp(reply.word, "gi"), (match) => `<mark>${match}</mark>`);
        document.getElementById("note").innerHTML = highlighted_content;
      });
    })
  }
}

The third argument in the pushEvent function is a callback that receives the response.

this.pushEvent("highlight", {note_id: el.dataset.note_id}, (reply, _ref) => {...});

The reply variable contains the extra information we send in the handle_event/3.

On the html side, we need to add the hook to a button.

<button
  id="highlight-button"
  phx-hook="Highlight"
  data-note_id={@note.id}
  type="button"
  class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
>
  <.icon name="hero-paint-brush-solid" class="h-5 w-5 text-zinc-900" />
</button>

And last, in the handle_event/3, instead of assigning the note to the socket, we reply with the keyword.

  @impl true
  def handle_event("highlight", %{"note_id" => note_id}, socket) do
    note = Resources.get_notes!(note_id)

    {:reply, %{word: note.keyword}, socket}
  end

If we check the developer tools in the browser, we see that we sent a tiny payload.

["4", "8", "lv:phx-F1wNVPrWLB4YvQKE", "phx_reply",…]
0 : "4"
1 : "8"
2 : "lv:phx-F1wNVPrWLB4YvQKE"
3 : "phx_reply"
4 : {response: {diff: {r: {word: "space"}}}, status: "ok"}

Although not so common, it’s a nice option to have in the toolbox. Sending less data over the wire is always a good option.

Stay up to date!

Get notified when I publish something new, and unsubscribe at any time.