zustand について
LobeHub開発向けに、Zustandの状態管理規約をまとめたスキルです。状態のみのストア、replicaによるサーバーデータ管理、既存ストアの編集や移行方法を扱います。
主な機能
- 01状態のみのストア構成を定義
- 02公開アクションと非公開アクションを分離
- 03replicaでサーバーデータを管理
- 04ハイドレーションと楽観的更新に対応
- 05既存ストアの規約に準拠
活用シーン
- 01Zustandストアの作成や移行
- 02replicaベースの一覧・詳細データを追加
- 03ロールバック付きの楽観的書き込みを実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lobehub/ lobehub --skill zustandインストール後、Claude Code で/zustandを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/data-structures.mdlegacy-slices.mdreferences/data-structures/reducer.mdtypes.md詳細
LobeHub Zustand 状態管理
状態の構造と型
- 共有ストアの型は
@lobechat/databaseではなく、@lobechat/typesからインポートしてください。 - 軽量なリスト項目の型と詳細情報全体の型は分けてください。リストの型は、重い詳細情報全体の型を継承してはいけません。
- キャッシュされた詳細情報は ID をキーにしてください(
xxxDetailMap: Record<string, Detail>)。単一のcurrentDetailオブジェクトを保持してはいけません。 - コンポーネント内で場当たり的なラムダ式を繰り返すのではなく、
xxxSelectorsの集約(selectors.ts)を通して読み取ってください。 - リスト/詳細の構造や型の参照元を決める前に、データ構造を読んでください。
状態のみのストア(新規ストアおよび移行後のストアでデフォルト)
アクションは Zustand の状態に含めません。参照先:src/store/page。
| ファイル | 所有するもの |
|---|---|
store.ts | useXStore = createWithEqualityFn<XState>()(devtools(() => initialState), shallow):状態のみ。メソッドは含めません |
privateAction.ts | XPrivateAction:レプリカのスライス、エンティティのリンク、およびパブリックアクション間で共有する書き込みヘルパー。index.ts からエクスポートしません |
action.ts | XActionImpl(パブリック API)と、シングルトンの xActions = new XActionImpl(setState, getState, new XPrivateAction(…)) |
- コンポーネントでは
useXStore(selector)で状態を読み取り、xActions.foo()を直接呼び出してください。ストアからアクションを選択してはいけません。 - 同期フックはパブリッククラスに置き、
xActions.useFetchX(params)のように呼び出します。 - パブリックアクション同士は
this.foo()を通じて呼び出します。プライベートヘルパーにはthis.#privateを通じてアクセスします。クラス間でロジックを共有するために、ストアにinternal_*メソッドを追加しないでください。 - すべての書き込みにラベルを付けてください:
this.#set(partial, false, n('actionName'))。ここでconst n = setNamespace('x')とします。 - パブリッククラスに
reset()を置き、src/store/utils/userDataStores.tsのresetableActionsにxActionsを登録してください。 - テストでは
xActionsを操作し、useXStore.getState()をアサートします。コンポーネントテストでモックする場合:vi.mock('@/store/x', () => ({ xActions: { foo: vi.fn() }, useXStore: … }))。
レプリカを基盤としたサーバーデータ
UI に表示するサーバーの一覧と詳細は、手書きの SWR onSuccess + reducer ではなく、@/libs/replica を経由します。エンジンが IndexedDB からのハイドレーション、ヘッドの再検証、ページング、ロールバック付きの楽観的オーバーレイ、スコープ(ユーザー+ワークスペース)の分離を担います。API リファレンスは、local-replica README を参照してください。エンジンは lobehub/local-replica で保守されています。そこで修正して公開し、こちらで依存関係を更新してください。
- ストアの
projection.tsでリソース(defineReplica/definePagedReplica)を定義し、境界でサーバーの行データを変換します。 - レプリカごとに状態フィールドを 2 つ追加します。ビュー(
xxxMap: Record<key, TData>)と、管理情報(xxxReplica: createReplicaState())です。 - プライベートクラスでバインドします。
createReplicaSlice(resource, { get, set, stateKey: 'xxxReplica', view: recordLens('xxxMap'), … })を使います。同じエンティティが複数のレプリカ(一覧行+詳細)に存在する場合は、linkReplicaEntity([list, detail])でリンクします。 - 読み込み:
slice.useSync(params)が返すのは取得状態のフラグ(isHydrated、isValidating、error、revalidate)のみです。データはxxxMapを参照するセレクターから取得します。「エントリーがundefined」であることが読み込み中の合図です。xxxInitフラグやloadingXxxIdsは追加しないでください。 - ビューをマウントせずに読み込む場合(ホバー時のプリフェッチ、ミューテーション後の更新):
await slice.fetch(params)を使います(書き込み後は{ fresh: true })。スコープ、削除、クリア、リクエストの置き換えに対するガードを回避してしまうため、サービスを直接呼び出してslice.replace(...)を手動で行わないでください。各レスポンスに伴う副作用(not-found マップ、別のストアへのシード処理)は、単一のフックのonSuccessではなく、isMissingを使ってスライスのeffects(onApplied/onMissing/onPurge/onError)に記述します。 - スケルトンなしでコールドスタートする場合:
useSyncによるハイドレーションはコンポーネントのマウント後にのみ行われます。ローカルコピーから描画する必要がある一覧では、preHydrateアクション(ensureScope+hydrate)を追加し、@/libs/replica/prePaintのsettleWithin(…, PRE_PAINT_HYDRATE_TIMEOUT)で時間を制限しながら、遅延ラッパー(src/spa/router/pageListLoader.ts、agentChatTopicListLoader.ts)を介してルートのloaderから呼び出します。@/libs/replicaはキャッシュスコープ、IndexedDB/localStorage ストレージ(@/libs/replica/storageのAppIndexedDBStorage/AppLocalStorage)、アプリの SWR ドライバーを設定します。パッケージの README はアプリに依存しない内容のままにしてください。 - 書き込み:
slice.optimistic(key, apply, serverCall)またはentity.optimistic(id, fn | 'remove', serverCall)を使います。呼び出しに失敗すると、確定済みの値にロールバックされるため、削除も楽観的に処理されます。クライアントにのみ存在する行(一時 ID)はisClientOnlyで宣言し、update(key, fn, { persist: false })でシードします。
テスト用のシード処理を除き、レプリカのビューへの書き込みは必ずそのスライスを通してください。通常の set では、エンジンの管理情報(永続化、ロールバックの基点、エンティティのリンク)が迂回されます。
レガシーストア
アクションクラスを状態にフラット化するストア(flattenActions、create*Slice、internal_*、internal_dispatch* + reducers、SWR onSuccess による同期)は、レガシースライスに従います。この構成は、そうしたストアを編集するときにのみ使用してください。データをレプリカに移行する際は、上記の構成に移行してください。