18.1 状態の分類
| 種類 | 例 | 主な置き場所 |
|---|---|---|
| ローカルUI state | モーダル、入力途中 | useState |
| 共有UI state | テーマ、選択中ID | 親、Context |
| サーバーstate | APIデータ | フレームワーク・取得ライブラリ |
| URL state | 検索、ページ番号 | ルーター |
| 永続クライアントstate | 設定 | Storage + 外部ストア |
| フォームstate | 入力、検証 | HTML/Action/フォームライブラリ |
すべてをグローバルストアへ入れない。
18.2 URLをstateとして使う
検索条件、ページ、タブなど共有・復元したい状態はURLへ置く。
/products?category=book&page=2&sort=price
利点:
- 再読み込みで復元
- ブックマーク可能
- 戻る・進むに対応
- URL共有可能
- サーバー側でも利用可能
18.3 サーバーstate
サーバー由来データには次の問題がある。
- キャッシュ
- stale判定
- 再検証
- 重複取得
- 楽観的更新
- ページネーション
- 失敗時再試行
- 認証期限
- オフライン
単純なuseEffect + useStateだけで全てを自作するより、利用フレームワークのローダーやキャッシュ機構を優先する。
18.4 コンポーネント層
一例:
Route/Page
├── Feature Container
│ ├── Domain Component
│ └── Form
└── Shared UI
├── Button
├── Dialog
└── Input
「Container/Presentational」を絶対規則にする必要はない。データ取得境界、状態所有者、再利用範囲が明確であることが重要。
18.5 Headless Component
見た目を固定せず、振る舞いとアクセシビリティを提供する。
function useDisclosure(initial = false) {
const [open, setOpen] = useState(initial);
return {
open,
openDisclosure: () => setOpen(true),
closeDisclosure: () => setOpen(false),
toggleDisclosure: () => setOpen(value => !value),
};
}
18.6 Compound Components
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">プロフィール</Tabs.Trigger>
<Tabs.Trigger value="security">セキュリティ</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile">...</Tabs.Panel>
<Tabs.Panel value="security">...</Tabs.Panel>
</Tabs>
柔軟性が高い一方、Context、型、アクセシビリティ、Server/Client境界が複雑になる。
18.7 エラー設計
- ユーザー入力エラー
- 認証・認可エラー
- 一時的ネットワークエラー
- データなし
- プログラム例外
- 部分的失敗
- 致命的失敗
すべてを「エラーが発生しました」にまとめず、ユーザーが次に取れる行動を示す。
18.8 読み込み設計
- 初回読み込み
- 更新中
- 追加読み込み
- バックグラウンド再検証
- Action送信中
- 楽観的状態
単一のisLoadingでは表現しきれないことが多い。状態機械として考える。