Elixir GenServer. Phoenix Live View and handling long-loading data
Wondering how to cope with long-loading data of your Elixir/Phoenix app? This is when GenServer in Phoenix Live View comes in handy. Let me show you how to use it!
According to the documentation, Phoenix Live View is supposed to be a rich & real-time user experience with aa server side rendered HTML. Thus, in my opinion, it should be as lightweight as possible. It cannot slow down, because it affects the user experience (UX).
But sometimes there are such circumstances when it is really unavoidable to load some thicker data. I came across such a problem while creating my previous application. Today I am going to show how to make a simple loading screen while the data is being assigned to the socket.
Whether it is going to be a really long calculation or a longish database query, the solution which came to my mind is really simple. It leverages the fact that the Live View underneath is nothing else than a process, which receives messages and updates its state. That is why we are going to use the GenServer utilities to make it work.
What is GenServer in Elixir?
GenServer is an Elixir abstraction wrapping around the process and its utilities. It keeps the state and has already implemented a set of tools to modify it. This includes tracking and reporting errors. Once you understand the basics of working with GenServers they become really comfortable to use. Live View is also taking advantage of GenServers and that is why we can use their functionalities in our case.
How do GenServers work?
GenServers implement 3 basic functions to handle messages: - `handle_cast` - when you don’t expect the response from the server, it handles the message sent by GenServer.cast/2 asynchronously, - `handle_call` - when you expect the response from the server, it handles synchronously the message sent by GenServer.call/2 synchronously, - `handle_info` - for all other messages that are not sent via GenServer.call/2 or GenServer.cast/2 functions, including messages sent via Kernel.send/2.
When it comes to the Phoenix Live View itself, it is nice to remember that the mount/3 function is invoked two times. First time when the page is rendered and the second time when the socket is connected. That is why you want to keep it fast - to improve the UX and keep your customers informed about what is happening.
Step-by-step data loading screen in an Elixir/Phoenix app
I made a really simple Phoenix application with a --live option. Then I removed a bit of the default code and started working on my loading screen in the default PageLive module.
Step 1 - mount/3 function
defmodule MyAppWeb.PageLive do
use LiveViewLoadingWeb, :live_view
...
def mount(_params, _session, socket) do
send(self(), :load_data)
{:ok, assign(socket, query: "", results: nil, loading: true)}
end
…
end
In the code listed above, I am sending a message via send/2 to the Live View process which initiates data loading to its state. Also, I am sending a socket response that has two keys assigned. The first one is loading, which indicates whether the data is loading. The second one is the data itself but as nil. It is there to avoid `ArgumentError` assign not available in the .leex file.
Step 2 - handle_info/2
defmodule MyAppWeb.PageLive do ... def handle_info(:load_data, socket) do Process.sleep(3000) # it imitates the long query to the database results = [%{name: "Monica", age: 22}, %{name: "Blanca", age: 21}] {:noreply, assign(socket, loading: false, results: results)} end … end
The code responsible for handling the `:load_data` message has only one functionality: to load heavy data to the socket. Once the data loading is finished, it sets the loading to false and sends the data via the socket to the user. In our case, the long loading is represented by the `Process.sleep/1` function.
Step 3 - .leex file
<%= if @loading do %>
Loading data...
<% else %>
<%= for result<-@results do %>
Name: <%= result.name %>, age: <%= result.age %> <br>
<% end %>
<% end %>
Finally, it is time to use our assigns in the corresponding `.leex` file. In this case, I used an `if` statement which decides whether the "Loading data…" message or results should be displayed. Naturally, there could be used some `render/3` functions, loading some more complicated partials, but for the purposes of this article, I simplified the code as much as I could.
Phoenix Live View: Long loading data and GenServer - Results
While entering the page we can see the "Loading data…"" screen.
Then, after a while, the data is displayed.
This solution can be used not only with the `mount/3` function.
If you need to load some other data later to the socket, you can use handle_event/3 and the same pattern - sending the message to the Phoenix Live View process in the previously mentioned handle_event/3 and handling it with handle_info/2. This is really useful, e.g. while doing pagination with heavy content.
Phoenix Live View and GenServer: Problems of this approach
I am aware that this approach has some limitations. The biggest downside in my opinion is that it is not really SEO-friendly. When the site is being visited by robots, they won’t see any loaded data, just the loading screen, because they are not waiting for the second `mount/3` invocation and for the data being loaded.
In the case of highly SEO-oriented endpoints, I would suggest just loading the data in the mount function.
How about your experiences with long-loading data in Elixir/Phoenix? Let me know in the comments section!