gio.wtf

MASTERING RECONNECTS

# Mastering Reconnects

---

## Slide 2

---

<.live_view_works_here yes={true} />

Work with us

Giovanni Francischelli

  • Brazilian ๐Ÿ‡ง๐Ÿ‡ท
  • Cinema ๐ŸŽฅ
  • Hiking โ›ฐ๏ธ
  • Swimming ๐Ÿ๏ธ

THE PROBLEM

PART I

Fixing my crappy slides

sequenceDiagram
  Server->>Server: mount(), handle_params(), render() 
  Server->>Browser: HTML v1
  Browser->>Browser: Changes: HTML v2
  Browser--xServer: Disconnect ๐Ÿ’ฅ
  Browser-->>Server: pls reconnect ๐Ÿ™
  Server->>Server: mount(), handle_params(), render() 
  Server->>Browser: HTML v1
  Browser->Browser: Reset ๐Ÿ˜ญ

Reconnect-fragility

def mount(_params, _session, socket) do
  {:ok, assign(socket, :active_slide, 0)}
end


def render(assigns) do
  ~H"""
  <.slide
      :for={{slide, index} <- Enum.with_index(@slides)}
      :if={index == @active_slide}
      data={slide}
  />
  """
end

THE FIX:

Store active slide in the URL

Use handle_params/3 to "re-mount" correctly

def handle_params(%{"index" => index}, _uri, socket) do
  {:ok, assign(socket, :active_slide, String.to_integer(index))}
end

sequenceDiagram
  Server->>Browser: HTML v1
  Browser->>Browser: Changes: HTML v2
  Browser--xServer: Disconnect ๐Ÿ’ฅ
  Browser-->>Server: pls reconnect ๐Ÿ™
  Server->Server: handle_params(slide=10)
  Server->>Browser: HTML v2
  Browser->Browser: PROFIT ๐Ÿค‘ 

Key Takeaways

  1. Fix UI resets by rendering the same HTML the browser has
  2. Use the DOM as a client data store
  3. Prefer CSS for conditional styling over assigns spaghetti
  4. Use connect_params to sync client state with server during reconnects
  5. Prefer "I haven't figured out" mindset over "It's a LiveView limitation"

PART II

When the URL is not an option

Fragile ๐Ÿ˜ญ

  • I'm Still Here
  • Chungking Express
  • The Straight Story
  • Indigo Blue
  • Rattlesnake
  • In the Garage

# Example Usage

<.live_component id="example" module={Tab}>
  <:tab name="Movies">
    <.movie_list />
  </:tab>

  <:tab name="Songs">
    <.song_list />
  </:tab>
</.live_component>

# TabLiveComponent.ex

def render(assigns) do
  ~H"""
  <button :for={tab <- @tab}
    class={tab.name == @active && "active"}
    phx-click="activate"
    phx-value-name={tab.name}>
    {tab.name}
  </button>

  <div :for={tab <- @tab}
    :if={panel.name == @active}>
    {render_slot(tab)}
  </div>
  """
end

def update(assigns, socket) do
  {:ok, assign_new(socket, :active, fn -> assigns.tab[0].name end)}
end

def handle_event("activate", params, socket) do
  {:noreply, assign(socket, active: params["name"])}
end

LiveComponent tabs recap

  • Resets assigns to initial state during reconnects
  • Server roundtrips for pure UI logic adds unnecessary latency
  • assigns based conditional styling

Resilient ๐Ÿ’ช

  • I'm Still Here
  • Chungking Express
  • The Straight Story
  • Indigo Blue
  • Rattlesnake
  • In the Garage

# Example Usage

<.tabs id="example">
  <:tab name="Movies">
    <.movie_list />
  </:tab>

  <:tab name="Songs">
    <.song_list />
  </:tab>
</.tabs>

def tabs(assigns) do
  assigns = assign_new(assigns, :initial_tab, fn -> assigns.tabs[0].name end)

  ~H"""
  <div id={@id} class="tabs" data-reset={reset()}>
    <button
      :for={{tab, index} <- Enum.with_index(@tab)}
      phx-click={change_tab(id, index)}
      data-active={tab.name == @initial_tab}
      class="data-active:bg-purple"
    >
      {tab.name}
    </button>

    <div
      :for={tab <- @tab}
      data-active={tab.name == @initial_tab}
      class="hidden data-active:block"
    >
      {render_slot(tab)}
    </div>
  </div>
  """
end

defp reset, do: JS.remove_attribute("data-active", to: {:inner, "[data-active]"})

defp change_tab(id, index) do
  JS.exec("data-reset", to: {:closest, ".tabs"})
  |> JS.set_attribute({"data-active", true}, to: "##{id} :nth-of-type(#{index + 1})")
end

Function component tabs

  • Pure UI interactions via client
  • Client-side state with HTML data attributes
  • CSS-only conditional styling

Conditional styling via data-* attributes

.panel {
  display: hidden;

  &[data-active] {
    display: block;
  }
}

More examples: styling the last element

Assigns based:

<%= for {item, index} <- Enum.with_index(@items) do %>
  <li class={index == length(@items) && "purple"}>
    {item}
  </li>
<% end %>

CSS based:

<li :for={item <- @items} class="breadcrumb">
  {item}
</li>
.breadcrumb:last-child {
  background-color: var(--bg-purple)
}

Even easier with Tailwind

<li class="bg-white last:bg-purple" />

Ok just one more CSS example

When there's an active code block, dim the inactive

.slide:has(code[data-active]) {
  code:not([data-active]) {
    opacity: 50%;
  }
}

Key Takeaways

  1. Fix UI resets by rendering the same HTML the browser has
  2. Use the DOM as a client data store
  3. Prefer CSS for conditional styling over assigns spaghetti
  4. Use connect_params to sync client state with server during reconnects
  5. Prefer "I haven't figured out" mindset over "It's a LiveView limitation"

Key Takeaways

  1. Fix UI resets by rendering the same HTML the browser has
  2. Use the DOM as a client data store
  3. Prefer CSS for conditional styling over assigns spaghetti
  4. Use connect_params to sync client state with server during reconnects
  5. Prefer "I haven't figured out" mindset over "It's a LiveView limitation"

PART III

When assigns are unavoidable

Unavoidable assigns

Stepper Demo

A

def render(assigns) do
  ~H"""
  <.panel step={@step} />
  """
end

def mount(_params, _session, socket) do
  {:ok, assign(socket, :step, 0)}
end

def handle_event("step", params, socket) do
  {:noreply, assign(socket, :step, params["index"])}
end

What if we could something like this?

# PSEUDO CODE
def mount(_params, _sesion, socket) do
  step =
    if reconnecting?(socket) do
      grab_step_from_browser_html(socket)
    end
  {:ok, assign(socket, :step, step || 0)}
end

TURNS OUT WE CAN

How to "grab data from browser" HTML tho?

Enter liveSocket

liveSocket.connect("/live", Socket, {
    params: {_csrf_token: csrf_token }
  }
)

Enter liveSocket

liveSocket.connect("/live", Socket, {
    params: (view) => {
      return {_csrf_token: csrf_token }
    }
  }
)

Sending arbitrary params during reconnects

liveSocket.connect("/live", Socket, {
    params: (view) => {
      return {
        _csrf_token: csrf_token,
        step: view.querySelector("[data-step]").dataset.step
      }
}});
<div data-step={@step}>

Reading the connect_params

def mount(_params, _session, socket) do
  connect_params = get_connect_params(socket)

  step = 
    if connect_params["_mounts"] > 0  do
      connect_params["step"]
    end

  {:ok, assign(socket, :step, step || 0 )}
end

A

sequenceDiagram
  Browser--xServer: Disconnect ๐Ÿ’ฅ
  Browser->>Browser: Recover data from DOM
  Browser-->>Server: pls reconnect ๐Ÿ™ (DATA)
  Server->Server: Read connect_params
  Server->>Browser: Correct HTML
  Browser->Browser: PROFIT ๐Ÿค‘

Key Takeaways

  1. Fix UI resets by rendering the same HTML the browser has
  2. Use the DOM as a client data store
  3. Prefer CSS for conditional styling over assigns spaghetti
  4. Use connect_params to sync client state with server during reconnects
  5. Prefer "I haven't figured out" mindset over "It's a LiveView limitation"

Key Takeaways

  1. Fix UI resets by rendering the same HTML the browser has
  2. Use the DOM as a client data store
  3. Prefer CSS for conditional styling over assigns spaghetti
  4. Use connect_params to sync client state with server during reconnects
  5. Prefer "I haven't figured out" mindset over "It's a LiveView limitation"

THANK YOU