loading...
Cover image for Phoenix Live View Debounce

Phoenix Live View Debounce

tizpuppi profile image Tiziano ・3 min read

Phoenix Live View is still in beta but a lot of people are building interesting stuff with it. At Simplificator we are also trying to use it for both external clients and internal projects.

Search form

One nice use case of the phoenix live view is to build a dynamic search form that performs some searches while typing.

As showed by Chris McChord in his examples this is easy to achieve with a live view:

def mount(_session, socket) do
  {:ok, assign(socket, query: nil, results: nil, loading: false)}
end

def handle_event("search", %{"query" => query}, socket) do
  send(self(), {:search, query})
  socket = socket
  |> assign(:query, query)
  |> assign(:loading, true)

  {:noreply, socket}
end

def handle_info({:search, query}, socket) do
  socket = socket
  |> assign(:loading, false)
  |> assign(results: search_results(query))
  {:noreply, socket}
end

The idea is to react on every key stroke by sending a message to the same process. This message is of the form {:search, query} and asynchronously performs the necessary query (for example on the database).

This is a pragmatic approach, but usually we want to avoid to perform too many unnecessary queries while typing. The possibility to set a minimum amount of time between two successive queries is called debounce and it is a very common feature in most javascript framework.

The debounce feature is not available at the moment in phoenix live view but will be implemented in the near future, as you can see from this discussion. Luckily this feature is very easy to implement.

Debounce

The biggest difference from the previous code snippet is that instead of sending a message to the current process, the message to perform the query is sent after a certain "debounce time" and the query term is stored in the state of the process (the socket in case of the phoenix live view).

This is achieved using Process.send_after/3 function which returns a timer reference. Then the timer reference is stored in the state (socket) of the live view along with a loading flag and the search term.

In case a new query term arrives within the "debounce time", the new query term is stored in the state. Once the "debounce time" is reached, the :search message is triggered, and the query performed.

Here you can see the full code snippet:

# new query term arrives within the debounce time
def handle_event("search", %{"query" => q}, %{assigns: %{loading: true}} = socket) do
  socket = socket
  |> assign(:query, q)

  {:noreply, socket}
end

def handle_event("search", %{"query" => q}, %{assigns: %{loading: false}} = socket) do
  # debounce time of 300 ms
  timer_ref = Process.send_after(self(), :search, 300)
  socket = socket
  |> assign(:query, q)
  |> assign(:timer_ref, timer_ref)
  |> assign(:loading, true)

  {:noreply, socket}
end

def handle_event("search-final", %{"query" => q}, socket)  do
  Process.cancel_timer(socket.assigns.timer_ref)
  socket = socket
  |> assign(:query, q)

  send(self(), :search)
  {:noreply, socket}
end

def handle_info(:search, socket) do
  socket = socket
  |> assign(:loading, false)
  |> assign(:results, search_results(socket.assigns.query))

  {:noreply, socket}
end

As you see the timer reference is used to cancel the future search request when the user will click on the search button and "commits" to a search; any future request is canceled (if any) and an immediate search performed.

Posted on May 26 '19 by:

tizpuppi profile

Tiziano

@tizpuppi

Elixir developer @ Simplificator (we are hiring)

Discussion

markdown guide
 

Beautiful!

Wouldn't the

socket = socket
  |> assign(:results, search_results(socket.assigns.query))
  |> assign(:loading, false)

make the debounce do its job better?

Argument being that expensive searches might last 1-200ms and in the meantime a new keystroke would appear

 

Thank you!

Actually the two implementation are the same because the assign function does not change socket in place, but build a new copy. The new socket is sent to the live view only when handle_info function completes.

Search_results in my implementation is a synchronous call and blocks the process until it returns. If search_results takes a very long time what happens is that the updated query terms queue in the mailbox of the process until the search_results function returns. You can see it very well if you put something like Process.sleep(10_000) inside the search_function.

 

:facepalm:

I'm a 12-year ruby veteran - and still breaking in my elixir-shoes

:$

 

Very nice!

I was looking into similar approach, but yours is much more elegant with Process.send_after.

However, it does not solve all problems. For example, I have a case where there is a text area and server does calculations on word count and few additional metrics. Even with this approach, whole text is being sent down the wire with every key stroke. So I hope we'll see proper debounce on frontend side soon ;)

 

Thank you,

indeed the whole text is send down the wire. This depends on the implementation of phoenix live view js side (phx-change in particular). Let's see how the 'official' debounce will be implemented :)