# State Management
Dialupの状態管理は `session`、`assigns`、`params` の3つに分かれている。
## 3種類の状態
| フィールド | 設定者 | ライフサイクル |
|-----------|--------|----------------|
| `session` | `layout.mount/1` | WebSocketセッション全体で持続 |
| `assigns` | `page.mount/2` | ナビゲーションごとにリセット |
| `params` | フレームワーク自動 | ナビゲーションごとに更新 |
### session
`layout.mount/1` が設定する。ページ遷移をまたいで持続するため、ログインユーザー情報などを格納する。
```elixir
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
end
```
レイアウトがネストしている場合、親レイアウトの結果が子レイアウトの `mount/1` に渡される。
### assigns
`page.mount/2` が設定する。ナビゲーションのたびにリセットされ、現在のページ固有の状態を保持する。
```elixir
defmodule Dialup.App.Users.Id.Page do
use Dialup.Page
def mount(%{"id" => id}, assigns) do
# assigns には session の内容が入っている(current_user 等を参照可能)
post = Posts.get!(id, assigns.current_user)
{:ok, %{post: post}} # 返り値が page assigns になる
end
end
```
`mount/2` の返り値はそのまま page assigns になる(session キーを含める必要はない)。
### params
URLパラメータ(パスパラメータとクエリパラメータの両方)をフレームワークが自動的に設定する。
```elixir
# /users/123?tab=posts にアクセスした場合
def mount(params, assigns) do
id = params["id"] # "123"(パスパラメータ)
tab = params["tab"] # "posts"(クエリパラメータ)
...
end
```
## テンプレートでのアクセス
render 直前に `session + assigns + params` がマージされるため、テンプレートではすべて `@` でアクセスできる。どのフィールド由来かを意識する必要はない。
```html
<p>{@current_user.name}</p> <!-- session 由来 -->
<h1>{@post.title}</h1> <!-- assigns 由来 -->
<p>ページ: {@params["page"]}</p> <!-- params -->
```
## handle_event での状態更新
`handle_event/3` には `session + assigns + params` がマージされた状態が渡される。返り値も同じ形式で返す。
```elixir
def handle_event("follow", _, assigns) do
# assigns には current_user(session由来)も post(assigns由来)も含まれる
new_post = Posts.follow(assigns.post, assigns.current_user)
{:update, Map.put(assigns, :post, new_post)}
end
```
フレームワークが `session_keys` を使って返り値を session と assigns に自動分割するため、開発者は意識不要。
### session を更新する
`handle_event` の返り値に session キーを含めれば session も更新できる。
```elixir
def handle_event("switch_locale", locale, assigns) do
{:update, Map.put(assigns, :locale, locale)}
# :locale が session_keys に含まれていれば session が更新される
end
```
## ナビゲーション時の挙動
```
/users/123 → /users/456 に遷移
session: 変化なし(current_user 等を保持)
assigns: リセット → page.mount/2 が再実行される
params: 新しいURLのパラメータに更新
```
session をリセットしたい場合は、`handle_event` や `layout.mount` 内で明示的に行う。
### ネスト layout の session マージ
初回接続では、ルートに含まれる **すべて** の layout の `mount/1` が親→子の順で実行されます。
ページ遷移時は session 全体を保持しつつ、**遷移先パスに初めて現れる layout だけ** `mount/1`
を呼びます。各 layout が返すキーのうち、まだ session に無いものだけが `Map.put_new` で追加されます。
すでにマウント済みの layout は再実行されないため、PubSub 購読などの副作用が毎回走ることはありません。
```elixir
# 例: /app → /app/settings(settings 配下にだけ Settings.Layout がある)
# Settings.Layout.mount/1 は初回遷移時のみ実行され、:settings_theme などが追加される
```
## 状態の揮発性
assigns・session はメモリ上に保持される。以下の場合に失われる:
- WebSocket切断後、タイムアウト(デフォルト5分)経過
- サーバー再起動
- プロセスクラッシュ
永続化が必要なデータはDBに保存すること。
### セッション永続化(ETS ストア)
プロセスがタイムアウトで停止する際にセッション状態を ETS に保存し、再接続時に復元するオプション。`use Dialup` に `session_store: :ets` を指定する。
```elixir
defmodule MyApp do
use Application
use Dialup,
app_dir: __DIR__ <> "/app",
title: "My App",
session_store: :ets # デフォルトは :memory(永続化なし)
@impl Application
def start(_type, _args) do
children = [{Dialup, app: __MODULE__, port: 4000}]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
```
`:ets` を指定すると:
- プロセスタイムアウト時に session / assigns / path が ETS テーブルに保存される
- 同じ `session_id` で再接続すると、保存された状態から復元される
- 復元後、ETS のエントリは自動的に削除される
ETS はノード再起動で消えるため、完全な永続化が必要な場合は DB を使用すること。
## 設計指針
**session に入れるもの**: ページをまたいで共有したい状態(ログインユーザー、言語設定など)
**assigns に入れるもの**: 現在のページ固有の状態(表示中のデータ、フォーム入力、UIフラグ)
**注意**: session と assigns で同じキー名を使うと session が優先される。命名の衝突を避けること。