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では表現しきれないことが多い。状態機械として考える。