Optimistic UI in LiveView

Hi!

I am implementing a Yatzy-style game to dig my teeth into LiveView.

When one player clicks on a die during their turn the state (and thus the appearance) of the die changes. Selecting and unselecting dice in Yatzy is the main player interaction and therefore the UI should be instant.

I have tried to implement an optimistic UI using Phoenix.LiveView.JS so that the appearance of a die is update immediately for the active player. It looks like this:

  attr :hold, :boolean, required: true
  attr :value, :integer, required: true
  attr :index, :integer, required: true
  attr :rolls_remaining, :integer, required: true

  def die(assigns) do
    ~H"""
    <button
      disabled={@rolls_remaining == 0 or @rolls_remaining == 3}
      phx-click={JS.push("toggle-die") |> optimistic_toggle(@hold, @index)}
      phx-value-die={@index}
    >
      <div class={if @hold, do: "", else: "roll duration-150"}>
        <div class={"face face-#{@index} p-1 rounded-lg border-4 w-16 h-16 #{if @hold, do: "bg-white/95 border-sky-800 translate-y-2", else: "bg-white border-transparent"}"}>
          <span
            :for={_n <- 1..@value}
            class="pip w-3 h-3 rounded-full self-center justify-self-center bg-black/95"
          />
        </div>
      </div>
    </button>
    """
  end

  def optimistic_toggle(js \\ %JS{}, hold, index) do
    to = ".face-#{index}"

    case hold do
      true ->
        js
        |> JS.remove_class("bg-white border-transparent", to: to)
        |> JS.add_class("bg-white/95 border-sky-800 translate-y-2", to: to)

      false ->
        js
        |> JS.remove_class("bg-white/95 border-sky-800 translate-y-2", to: to)
        |> JS.add_class("bg-white border-transparent", to: to)
    end
  end

I would expect the classes to be removed/added immediately, however this doesn’t to make a difference. I have tested this with liveSocket.enableLatencySim(100).

Anybody spot what I am doing wrong? Am I using Phoenix.LiveView.JS incorrectly?

Much appreciated,
David

Have you tried swapping these two operations? Seems like you want to update the classes in the UI before sending data to the server.

It doesn’t make a difference.

I am starting to wonder if liveSocket.enableLatencySim(100) also simulates latency on the LiveView.JS calls even though it shouldn’t :thinking: