liveview-lifecycle

LiveView Rendering Lifecycle

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "liveview-lifecycle" with this command: npx skills add j-morgan6/elixir-claude-optimization/j-morgan6-elixir-claude-optimization-liveview-lifecycle

LiveView Rendering Lifecycle

Critical Understanding

LiveView renders happen in TWO phases:

Static/Disconnected Render - Initial HTTP request, server-side HTML

  • No WebSocket connection

  • No live functionality yet

  • connected?(socket) returns false

Connected Render - WebSocket established, full LiveView active

  • Live updates work

  • Events are handled

  • connected?(socket) returns true

The Problem: Uninitialized Assigns

During static render, socket assigns may not be fully initialized:

❌ DANGEROUS - Can crash during static render

def render(assigns) do user_name = assigns.current_user.name # KeyError if not set! ~H"<p>Hello <%= user_name %></p>" end

Error: KeyError: key :current_user not found

The Solution: Defensive Access

Always use safe access patterns:

✅ SAFE - Works in both render phases

def render(assigns) do user_name = Map.get(assigns, :current_user, %{name: "Guest"}).name ~H"<p>Hello <%= user_name %></p>" end

✅ BETTER - Initialize in mount

@impl true def mount(_params, session, socket) do socket = assign(socket, :current_user, get_user(session)) {:ok, socket} end

Best Practices

  1. Initialize All Assigns in mount/3

Always initialize every assign you'll use:

@impl true def mount(_params, _session, socket) do socket = socket |> assign(:user, nil) |> assign(:loading, false) |> assign(:data, []) |> assign(:error, nil)

{:ok, socket} end

  1. Use Map.get for Optional Assigns

When accessing assigns that might not exist:

❌ BAD

defp render_user(socket) do socket.assigns.current_user.name end

✅ GOOD

defp render_user(socket) do case Map.get(socket.assigns, :current_user) do nil -> "Guest" user -> user.name end end

✅ ALSO GOOD

defp render_user(socket) do Map.get(socket.assigns, :current_user, %{name: "Guest"}).name end

  1. Use Pattern Matching Safely

❌ BAD - Crashes if not a map with :name

defp format_user(%{name: name}), do: name

✅ GOOD - Handles nil case

defp format_user(%{name: name}), do: name defp format_user(_), do: "Unknown"

✅ ALSO GOOD - Check first

defp format_user(user) when is_map(user), do: Map.get(user, :name, "Unknown") defp format_user(_), do: "Unknown"

  1. Use assigns_to_attributes for Components

Component with dynamic assigns

def card(assigns) do ~H""" <div class="card" {@rest}> <%= render_slot(@inner_block) %> </div> """ end

Usage

<.card id="my-card" data-role="admin"> Content </.card>

Connected Check

Use connected?/1 for operations that only work with WebSocket:

@impl true def mount(_params, _session, socket) do socket = if connected?(socket) do # Only run when WebSocket is connected Phoenix.PubSub.subscribe(MyApp.PubSub, "updates") schedule_refresh() socket else # Static render - skip expensive operations socket end

socket = assign(socket, :data, load_initial_data()) {:ok, socket} end

Why? PubSub subscriptions, timers, and live updates only work when connected.

Handle Params Considerations

handle_params/3 is called in both render phases:

@impl true def handle_params(%{"id" => id}, _uri, socket) do

This runs during static AND connected render

post = Posts.get_post!(id) # OK - database queries work in both phases

if connected?(socket) do # Only subscribe when connected Phoenix.PubSub.subscribe(MyApp.PubSub, "post:#{id}") end

{:noreply, assign(socket, :post, post)} end

Common Lifecycle Mistakes

❌ Mistake 1: Assuming Assigns Exist

def render(assigns) do ~H""" <p>Count: <%= @count %></p> <!-- Crash if @count not initialized --> """ end

✅ Fix: Initialize in mount

@impl true def mount(_params, _session, socket) do {:ok, assign(socket, :count, 0)} end

def render(assigns) do ~H""" <p>Count: <%= @count %></p> <!-- Safe now --> """ end

❌ Mistake 2: Subscribing in Both Phases

@impl true def mount(_params, _session, socket) do

BAD - Subscribes during static render (doesn't work)

Phoenix.PubSub.subscribe(MyApp.PubSub, "topic") {:ok, socket} end

✅ Fix: Check connected?

@impl true def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(MyApp.PubSub, "topic") end

{:ok, socket} end

❌ Mistake 3: Expensive Operations in Static Render

@impl true def mount(_params, _session, socket) do

BAD - Runs expensive query twice (static + connected)

data = run_expensive_query() {:ok, assign(socket, :data, data)} end

✅ Fix: Defer to connected phase

@impl true def mount(_params, _session, socket) do socket = if connected?(socket) do # Only run expensive operations when connected assign(socket, :data, run_expensive_query()) else # Use placeholder data for static render assign(socket, :data, []) end

{:ok, socket} end

Lifecycle Flow

  1. HTTP Request arrives ↓
  2. mount/3 called (connected? = false) ↓
  3. handle_params/3 called (connected? = false) ↓
  4. render/1 called (STATIC HTML generated) ↓
  5. HTML sent to browser ↓
  6. Browser connects WebSocket ↓
  7. mount/3 called AGAIN (connected? = true) ↓
  8. handle_params/3 called AGAIN (connected? = true) ↓
  9. render/1 called (sent over WebSocket) ↓
  10. LiveView now active and reactive

Debugging Tips

Check if LiveView is connected

def render(assigns) do ~H""" <div data-connected={@connected?}> <!-- Shows connection state --> </div> """ end

@impl true def mount(_params, _session, socket) do {:ok, assign(socket, :connected?, connected?(socket))} end

Log render phases

@impl true def mount(_params, _session, socket) do IO.puts("Mount called - Connected: #{connected?(socket)}") {:ok, socket} end

@impl true def handle_params(params, _uri, socket) do IO.puts("Handle params - Connected: #{connected?(socket)}") {:noreply, socket} end

Safe Assign Access Helpers

Create helper functions for safe access:

defp get_assign(socket, key, default \ nil) do Map.get(socket.assigns, key, default) end

defp has_assign?(socket, key) do Map.has_key?(socket.assigns, key) end

Usage

def some_function(socket) do if has_assign?(socket, :current_user) do user = get_assign(socket, :current_user) # Do something with user end end

Testing Both Phases

test "renders correctly in both phases", %{conn: conn} do

Static render (disconnected)

{:ok, _view, html} = live(conn, "/page") assert html =~ "Expected content"

Now connected

Can test live interactions

end

test "initializes assigns in mount", %{conn: conn} do {:ok, view, _html} = live(conn, "/page")

Check assigns are set

assert view.assigns.count == 0 assert view.assigns.user != nil end

Quick Reference

Safe Patterns

✅ Initialize in mount

assign(socket, :key, default_value)

✅ Use Map.get for optional

Map.get(socket.assigns, :key, default)

✅ Check connected for side effects

if connected?(socket), do: subscribe()

✅ Pattern match with fallback

def helper(%{name: name}), do: name def helper(_), do: "default"

Unsafe Patterns

❌ Direct access without initialization

socket.assigns.key

❌ Subscribe without checking

Phoenix.PubSub.subscribe(...)

❌ Expensive ops in both phases

mount(...) do data = expensive_query() end

❌ Pattern match without fallback

def helper(%{name: name}), do: name

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

elixir-essentials

No summary provided by upstream source.

Repository SourceNeeds Review
General

phoenix-liveview

No summary provided by upstream source.

Repository SourceNeeds Review
General

elixir-patterns

No summary provided by upstream source.

Repository SourceNeeds Review