liveview-lifecycle

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

LiveView Rendering Lifecycle

LiveView 渲染生命周期

Critical Understanding

关键知识点

LiveView renders happen in TWO phases:
  1. Static/Disconnected Render - Initial HTTP request, server-side HTML
    • No WebSocket connection
    • No live functionality yet
    • connected?(socket)
      returns
      false
  2. Connected Render - WebSocket established, full LiveView active
    • Live updates work
    • Events are handled
    • connected?(socket)
      returns
      true
LiveView 的渲染分为两个阶段:
  1. 静态/断开连接渲染 - 初始HTTP请求,服务端生成HTML
    • 无WebSocket连接
    • 暂不具备实时功能
    • connected?(socket)
      返回
      false
  2. 连接后渲染 - WebSocket已建立,LiveView完全激活
    • 支持实时更新
    • 可处理事件
    • connected?(socket)
      返回
      true

The Problem: Uninitialized Assigns

问题:未初始化的Assign

During static render, socket assigns may not be fully initialized:
elixir
undefined
在静态渲染期间,socket的assign可能尚未完全初始化:
elixir
undefined

❌ 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`
def render(assigns) do user_name = assigns.current_user.name # 若未设置会触发KeyError! ~H"<p>Hello <%= user_name %></p>" end

**错误:** `KeyError: key :current_user not found`

The Solution: Defensive Access

解决方案:防御式访问

Always use safe access patterns:
elixir
undefined
请始终使用安全的访问模式:
elixir
undefined

✅ 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
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

✅ 更优方案 - 在mount中初始化

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

Best Practices

最佳实践

1. Initialize All Assigns in mount/3

1. 在mount/3中初始化所有Assign

Always initialize every assign you'll use:
elixir
@impl true
def mount(_params, _session, socket) do
  socket =
    socket
    |> assign(:user, nil)
    |> assign(:loading, false)
    |> assign(:data, [])
    |> assign(:error, nil)

  {:ok, socket}
end
请始终初始化所有需要用到的assign:
elixir
@impl true
def mount(_params, _session, socket) do
  socket =
    socket
    |> assign(:user, nil)
    |> assign(:loading, false)
    |> assign(:data, [])
    |> assign(:error, nil)

  {:ok, socket}
end

2. Use Map.get for Optional Assigns

2. 对可选Assign使用Map.get

When accessing assigns that might not exist:
elixir
undefined
当访问可能不存在的assign时:
elixir
undefined

❌ BAD

❌ 不良写法

defp render_user(socket) do socket.assigns.current_user.name end
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
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
undefined
defp render_user(socket) do Map.get(socket.assigns, :current_user, %{name: "Guest"}).name end
undefined

3. Use Pattern Matching Safely

3. 安全使用模式匹配

elixir
undefined
elixir
undefined

❌ BAD - Crashes if not a map with :name

❌ 不良写法 - 若不是包含:name的map会崩溃

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

✅ GOOD - Handles nil case

✅ 正确写法 - 处理nil情况

defp format_user(%{name: name}), do: name defp format_user(_), do: "Unknown"
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"
undefined
defp format_user(user) when is_map(user), do: Map.get(user, :name, "Unknown") defp format_user(_), do: "Unknown"
undefined

4. Use assigns_to_attributes for Components

4. 对组件使用assigns_to_attributes

elixir
undefined
elixir
undefined

Component with dynamic assigns

带动态assign的组件

def card(assigns) do ~H"""
<div class="card" {@rest}> <%= render_slot(@inner_block) %> </div> """ end
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>
undefined
<.card id="my-card" data-role="admin"> Content </.card>
undefined

Connected Check

连接状态检查

Use
connected?/1
for operations that only work with WebSocket:
elixir
@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.
对于仅在WebSocket连接下才能正常工作的操作,请使用
connected?/1
elixir
@impl true
def mount(_params, _session, socket) do
  socket =
    if connected?(socket) do
      # 仅当WebSocket连接建立后才执行
      Phoenix.PubSub.subscribe(MyApp.PubSub, "updates")
      schedule_refresh()
      socket
    else
      # 静态渲染 - 跳过耗时操作
      socket
    end

  socket = assign(socket, :data, load_initial_data())
  {:ok, socket}
end
原因: PubSub订阅、定时器和实时更新只有在连接建立后才能正常工作。

Handle Params Considerations

Handle Params注意事项

handle_params/3
is called in both render phases:
elixir
@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
handle_params/3
会在两个渲染阶段都被调用:
elixir
@impl true
def handle_params(%{"id" => id}, _uri, socket) do
  # 此代码会在静态渲染和连接后渲染阶段都执行
  post = Posts.get_post!(id)  # 没问题 - 数据库查询在两个阶段都能执行

  if connected?(socket) do
    # 仅在连接建立后才订阅
    Phoenix.PubSub.subscribe(MyApp.PubSub, "post:#{id}")
  end

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

Common Lifecycle Mistakes

常见生命周期错误

❌ Mistake 1: Assuming Assigns Exist

❌ 错误1:假设Assign已存在

elixir
def render(assigns) do
  ~H"""
  <p>Count: <%= @count %></p>  <!-- Crash if @count not initialized -->
  """
end
elixir
def render(assigns) do
  ~H"""
  <p>Count: <%= @count %></p>  <!-- 若@count未初始化会崩溃 -->
  """
end

✅ Fix: Initialize in mount

✅ 修复方案:在mount中初始化

elixir
@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
elixir
@impl true
def mount(_params, _session, socket) do
  {:ok, assign(socket, :count, 0)}
end

def render(assigns) do
  ~H"""
  <p>Count: <%= @count %></p>  <!-- 现在安全了 -->
  """
end

❌ Mistake 2: Subscribing in Both Phases

❌ 错误2:在两个阶段都执行订阅

elixir
@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
elixir
@impl true
def mount(_params, _session, socket) do
  # 不良写法 - 在静态渲染阶段执行订阅(无法正常工作)
  Phoenix.PubSub.subscribe(MyApp.PubSub, "topic")
  {:ok, socket}
end

✅ Fix: Check connected?

✅ 修复方案:检查连接状态

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

  {:ok, socket}
end
elixir
@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

❌ 错误3:在静态渲染阶段执行耗时操作

elixir
@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
elixir
@impl true
def mount(_params, _session, socket) do
  # 不良写法 - 耗时查询会执行两次(静态渲染+连接后渲染)
  data = run_expensive_query()
  {:ok, assign(socket, :data, data)}
end

✅ Fix: Defer to connected phase

✅ 修复方案:延迟到连接后阶段执行

elixir
@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
elixir
@impl true
def mount(_params, _session, socket) do
  socket =
    if connected?(socket) do
      # 仅在连接建立后才执行耗时操作
      assign(socket, :data, run_expensive_query())
    else
      # 静态渲染使用占位数据
      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
1. HTTP 请求到达
2. 调用mount/3(connected? = false)
3. 调用handle_params/3(connected? = false)
4. 调用render/1(生成静态HTML)
5. 将HTML发送至浏览器
6. 浏览器建立WebSocket连接
7. 再次调用mount/3(connected? = true)
8. 再次调用handle_params/3(connected? = true)
9. 调用render/1(通过WebSocket发送)
10. LiveView 现在已激活并具备响应式能力

Debugging Tips

调试技巧

Check if LiveView is connected

检查LiveView是否已连接

elixir
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
elixir
def render(assigns) do
  ~H"""
  <div data-connected={@connected?}>
    <!-- 显示连接状态 -->
  </div>
  """
end

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

Log render phases

记录渲染阶段

elixir
@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
elixir
@impl true
def mount(_params, _session, socket) do
  IO.puts("调用Mount - 已连接: #{connected?(socket)}")
  {:ok, socket}
end

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

Safe Assign Access Helpers

安全Assign访问工具函数

Create helper functions for safe access:
elixir
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
可以创建工具函数来实现安全访问:
elixir
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
undefined
def some_function(socket) do if has_assign?(socket, :current_user) do user = get_assign(socket, :current_user) # 处理用户数据 end end
undefined

Testing Both Phases

测试两个阶段

elixir
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
elixir
test "在两个阶段都能正确渲染", %{conn: conn} do
  # 静态渲染(断开连接状态)
  {:ok, _view, html} = live(conn, "/page")
  assert html =~ "预期内容"

  # 现在处于连接状态
  # 可以测试实时交互
end

test "在mount中初始化assign", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/page")

  # 检查assign是否已设置
  assert view.assigns.count == 0
  assert view.assigns.user != nil
end

Quick Reference

快速参考

Safe Patterns

安全模式

elixir
undefined
elixir
undefined

✅ Initialize in mount

✅ 在mount中初始化

assign(socket, :key, default_value)
assign(socket, :key, default_value)

✅ Use Map.get for optional

✅ 对可选值使用Map.get

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

✅ Check connected for side effects

✅ 对副作用操作检查连接状态

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

✅ Pattern match with fallback

✅ 带降级处理的模式匹配

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

Unsafe Patterns

不安全模式

elixir
undefined
elixir
undefined

❌ Direct access without initialization

❌ 未初始化直接访问

socket.assigns.key
socket.assigns.key

❌ Subscribe without checking

❌ 未检查连接状态就执行订阅

Phoenix.PubSub.subscribe(...)
Phoenix.PubSub.subscribe(...)

❌ Expensive ops in both phases

❌ 在两个阶段都执行耗时操作

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

❌ Pattern match without fallback

❌ 无降级处理的模式匹配

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