ページのライフサイクル(mount → render → handle_event/handle_info)について。
ライフサイクル概要
初回接続: WebSocket接続 → layout.mount/1(session確定)→ page.mount/2(assigns確定)→ render
ページ遷移: __navigate → page.mount/2(assigns リセット)→ render
イベント: handle_event/3 → {:update, _} なら render
再接続: プロセス生存中なら mount なしで現在の state で再描画layout.mount/1
def mount(session :: map()) :: {:ok, map()}WebSocket接続時に一度だけ呼ばれる。ページ遷移では再呼び出しされない。
session: 親レイアウトが設定した session(最上位レイアウトは%{}を受け取る)- 戻り値:
{:ok, new_session}で session に追加するキーを返す
defmodule Dialup.App.Layout do
use Dialup.Layout
def mount(session) do
user = Repo.get(User, get_user_id_from_cookie())
{:ok, Map.put(session, :current_user, user)}
end
def render(assigns) do
~H"""
<nav>{@current_user.name}</nav>
<main>{raw(@inner_content)}</main>
"""
end
endmount/1 を定義しない場合はデフォルト実装(session をそのまま返す)が使われる。
page.mount/2
def mount(params :: map(), assigns :: map()) ::
{:ok, map()}
| {:redirect, path :: binary()}
| {:redirect, path :: binary(), map()}ナビゲーションのたびに呼ばれる。assigns は毎回リセットされてから再設定される。
params: URLパラメータ(パスパラメータ+クエリパラメータ)assigns: session の内容(current_user等を参照可能)- 戻り値:
{:ok, new_assigns}— 返したマップが page assigns になる{:redirect, path}— レンダリング前に別パスへ遷移(session は保持){:redirect, path, merged}— session キーを含む merged map を更新してから遷移
def mount(%{"id" => id}, assigns) do
post = Posts.get!(id, assigns.current_user)
{:ok, %{post: post}}
endmount でのリダイレクト(認証ガードなど)
handle_event/3 と同様、mount/2 から {:redirect, path} を返せる。HTTP GET・WebSocket 遷移・
/.well-known/dialup-agent の discovery 解決のいずれでも、レンダリング前に安全な相対パスへ
送られる。
def mount(_params, session) do
if session[:current_user] do
{:ok, session}
else
{:redirect, "/log_in"}
end
endmix dialup.gen.auth が生成する /settings ページはこのパターンを使う。詳細は
Authentication を参照。
mount/1(静的ページ用)
params が不要なページでは mount/1 を定義できる。mount/2 は自動生成される。
def mount(assigns) do
{:ok, %{items: Items.all()}}
endmount の呼び出しタイミング
| タイミング | layout.mount | page.mount |
|---|---|---|
| 初回 WebSocket 接続 | 呼ばれる | 呼ばれる |
| ページ遷移(ws-href) | 呼ばれない | 呼ばれる |
| 再接続(プロセス生存中) | 呼ばれない | 呼ばれない |
| 再接続(タイムアウト済み) | 呼ばれる | 呼ばれる |
handle_event/3
def handle_event(event :: String.t(), value :: any(), assigns :: map()) ::
{:noreply, map()} | {:update, map()} |
{:patch, String.t(), any(), map()} | {:redirect, String.t(), map()} |
{:push_event, String.t(), map(), map()}assigns には session + assigns + params がマージされた状態が渡される。
返り値
| 返り値 | 動作 |
|---|---|
{:noreply, assigns} | 状態のみ更新、再描画なし |
{:update, assigns} | 全体を再描画 |
{:patch, target_id, html, assigns} | 指定IDの要素のみ更新 |
{:redirect, path, assigns} | 別URLへ遷移(session保持、assigns リセット) |
{:push_event, name, payload, assigns} | JSフックを呼び出す(全体再描画も行う) |
# 再描画なし(頻繁なイベントに)
def handle_event("draft", value, assigns) do
{:noreply, Map.put(assigns, :draft, value)}
end
# 全体再描画
def handle_event("submit", _value, assigns) do
{:update, Map.put(assigns, :submitted, true)}
end
# 部分更新(効率的)
defp render_counter(assigns) do
~H"""<span id="counter">{@count}</span>"""
end
def handle_event("inc", _value, assigns) do
new_assigns = Map.update!(assigns, :count, &(&1 + 1))
{:patch, "counter", render_counter(new_assigns), new_assigns}
end
# 別ページへリダイレクト
def handle_event("logout", _value, assigns) do
{:redirect, "/login", Map.delete(assigns, :current_user)}
endhandle_info/2
def handle_info(msg :: any(), assigns :: map()) ::
{:noreply, map()} | {:update, map()} |
{:patch, String.t(), any(), map()} | {:redirect, String.t(), map()} |
{:push_event, String.t(), map(), map()}Process.send_after/3 や Phoenix.PubSub などからのメッセージを受け取る。返り値は handle_event/3 と同じ。
def mount(_params, assigns) do
# 5秒ごとにリフレッシュ
Process.send_after(self(), :refresh, 5_000)
{:ok, %{data: Data.fetch()}}
end
def handle_info(:refresh, assigns) do
Process.send_after(self(), :refresh, 5_000)
{:update, Map.put(assigns, :data, Data.fetch())}
enddef mount(_params, assigns) do
# subscribe/2 を使うとナビゲーション時に自動 unsubscribe される
subscribe(MyApp.PubSub, "room:lobby")
{:ok, %{messages: []}}
end
def handle_info({:new_message, msg}, assigns) do
{:update, Map.update!(assigns, :messages, &[msg | &1])}
endsubscribe/2 で登録したトピックは別ページへのナビゲーション時にフレームワークが自動的に unsubscribe する。handle_info/2 を定義しない場合はデフォルト実装(:noreply)が使われる。
エラー時の挙動
mount・handle_event・handle_info の中で例外が発生した場合:
- GenServer はクラッシュせず、プロセスは生存し続ける
- エラー内容(メッセージ+スタックトレース)をブラウザに表示する
handle_event/handle_infoのエラーは元の state に戻す(assigns は変化しない)mountのエラーはページが未確定のまま待機状態になる
page_title/1
ページごとに <title> を動的に設定するオプショナルコールバック。
def page_title(assigns) :: binary() | nilassigns の現在値を受け取り、タイトル文字列を返す。nil を返すと use Dialup で設定した title オプションがそのまま使われる。
defmodule Dialup.App.Users.Id.Page do
use Dialup.Page
def mount(%{"id" => id}, assigns) do
user = Users.get!(id)
{:ok, %{user: user}}
end
def page_title(assigns) do
"#{assigns.user.name} | My App"
end
def render(assigns) do
~H"""
<h1>{@user.name}</h1>
"""
end
end動作の仕組み
- SSR(初回 HTTP):
page_title/1の結果が Shell の<title>に反映される - WebSocket 更新: サーバーからの JSON メッセージに
titleフィールドが含まれ、dialup.jsがdocument.titleを自動的に更新する - ナビゲーション: ページ遷移のたびに新しいページの
page_title/1が呼ばれ、タイトルが更新される
page_title/1 を定義しない場合はデフォルト実装(nil を返す)が使われ、タイトルは変化しない。
render/1
def render(assigns) :: Phoenix.LiveView.Rendered.t()assigns の現在値を使ってHTMLを生成する。~H シジルまたは .html.heex ファイルを使用。
def render(assigns) do
~H"""
<div>
<p>Count: {@count}</p>
<%= if @loaded do %>
<p>Loaded!</p>
<% end %>
</div>
"""
end注意: ~H はローカル変数を直接参照できない(HEExのchange tracking制約)。必ず assigns を引数に取る関数内で使用し、@field_name でアクセスすること。