Show random quote in a liveview

I have a list of random quotes, and I want to pick a random one and display it on a liveview page.

I have this line in the mount function:

    socket = assign(socket, quote: random_quote())

random_quote uses Enum.random to choose a random quote from the list.

The problem is that the static render and liveview render may pick different quotes, resulting in the page showing one briefly before switching to a different quote.

I don’t want to display the quote conditionally, I want it to be visible on initial render to avoid unnecessary layout shifts.

How can I best solve this?

One option is this: Phoenix.LiveView — Phoenix LiveView v1.0.11

This is the simplest solution but doesn’t quite satisfy @andzdroid’s requirements of it being rendered on the dead render as well. The “thing” about LiveView is that it stores its state on the server so I don’t believe there is a simple solution to this problem. You could look into if there is a way to shove things into cookies/sessions on the dead render from an LV (not sure if possible since you only have a socket and not a conn at this point) then immediately read it back when connected. Other options would involve getting JS (localStorage, IndexedDB, etc) or the backend database involved. Perhaps someone else has better ideas?

@jdiago’s suggestion is the really the way to go unless you have a really good reason for wanting it to be present on the dead render. I can’t quite imagine what… a random string isn’t going to matter to search engine indexers.

I think I would have index and show actions. In the mount/3 for the index action: select random index of quotes and push_patch to the show action with that quote index (sorry one too many meanings of “index” here).

What if you render a placeholder on the dead view, that would probably reduce the layout shift.

Here’s my attempt:

defmodule YourProjectWeb.QuoteLive do
  use YourProjectWeb, :live_view

  @quotes [
    ~s("Do or do not. There is no try." - Yoda),
    ~s("You miss 100% of the shots you don't take." - Wayne Gretzky),
    ~s("Be the change you want to see in the world." - Not actually Mahatma Gandhi),
    ~s("The only way to do great work is to love what you do." - Steve Jobs)
  ]

  @impl true
  def mount(_params, _session, socket) do
    random_quote =
      @quotes
      |> Enum.map(fn quote -> {quote, String.jaro_distance(quote, socket.id)} end)
      |> Enum.sort(fn {_quote_1, jaro_distance_1}, {_quote_2, jaro_distance_2} ->
        jaro_distance_1 > jaro_distance_2
      end)
      |> List.first()
      |> elem(0)

    {:ok, assign(socket, quote: random_quote)}
  end

  @impl true
  def render(assigns) do
    ~H"{@quote}"
  end
end

From what I can tell, the socket ID seems to stay the same for both the dead and live renders, so if you get the string that most closely matches the socket ID (which I believe is random enough for your needs), then you can use that to get the same result for both renders, while still preserving randomness, without having to do too much extra work.

1 Like

The socket id is perfect, thanks!

I ended up with this:

    index = :erlang.phash2(socket.id, length(quotes))
    quote = Enum.at(quotes, index)
3 Likes

I have a follow-up. It seems the socket id is persistent across page navigations as well, as long as the liveview connection is the same. So this means every time the same user/connection navigates to this page, they will always see the same quote.

Ideally I want to show a different quote each time the user navigates to the page.

Any ideas on how to do this?

As @sodapopcan mentioned, you could get JavaScript involved. Fly.io has a guide for a generalized solution using sessionStorage, but I think your use case is simpler than that.

If you look at app.js and how it sends up the CSRF token, you could do something similar with your quote on the dead render. Include a “quote ID” of some kind as an attribute when you initially render the page. Then have your JavaScript look for that element using querySelector and include the “ID” as a param when you connect liveSocket. Then that ID is available in your LiveView with get_connect_params/1, and you can look up and render the same quote for the live render.

2 Likes

I would just use a GenServer. On unconnected mount, it would choose a random quote and store the association between socket ID and the quote index. On connected mount, it would return the quote at stored index (defaulting to selecting a random quote for live navigation case ) and remove the association. Some sort of garbage collection would be needed though, in case connected mount does not happen for some reason

1 Like