zustand

zustand

熱門

LobeHub 的 Zustand store 開發規範。適用於修改 src/store、store slice、公開/內部 action、dispatch action、flattenActions、樂觀更新(optimistic updates)、selector、map 或類別化 action(class action)遷移等情境。

8.1萬星標
1.6萬分支
更新於 2026/7/23
SKILL.md
唯讀
名稱
zustand
描述

LobeHub 的 Zustand store 開發規範。適用於修改 src/store、store slice、公開/內部 action、dispatch action、flattenActions、樂觀更新(optimistic updates)、selector、map 或類別化 action(class action)遷移等情境。

LobeHub Zustand 狀態管理

Action 類型層級

1. 公開 Action (Public Actions)

供 UI 元件呼叫的主要介面:

  • 命名:動詞形式(createTopicsendMessage
  • 職責:參數驗證、流程編排

2. 內部 Action (internal_*)

核心業務邏輯的具體實現:

  • 命名:使用 internal_ 前綴(internal_createTopic
  • 職責:樂觀更新、服務呼叫、錯誤處理
  • 注意:不應直接由 UI 呼叫

3. Dispatch 方法 (internal_dispatch*)

狀態更新的處理函式:

  • 命名:internal_dispatch + 實體(internal_dispatchTopic
  • 職責:呼叫 reducer、更新 store 狀態

何時使用 Reducer 與簡單 set

使用 Reducer 模式:

  • 管理物件陣列 / Map(如 messagesMaptopicMaps
  • 樂觀更新
  • 複雜的狀態過渡與變更

使用簡單 set

  • 切換布林值
  • 更新簡單數值
  • 設定單一狀態欄位

樂觀更新模式 (Optimistic Update Pattern)

internal_createTopic: async (params) => {
  const tmpId = Date.now().toString();

  // 1. 立即更新前端(樂觀更新)
  get().internal_dispatchTopic(
    { type: 'addTopic', value: { ...params, id: tmpId } },
    'internal_createTopic'
  );

  // 2. 呼叫後端服務
  const topicId = await topicService.createTopic(params);

  // 3. 重新整理以保持資料一致性
  await get().refreshTopic();
  return topicId;
},

刪除操作:請勿使用樂觀更新(具破壞性且復原邏輯複雜)

命名規範

Action:

  • 公開:createTopicsendMessage

  • 內部:internal_createTopicinternal_updateMessageContent

  • Dispatch:internal_dispatchTopic

State(狀態):

  • ID 陣列:topicEditingIds

  • Map:topicMapsmessagesMap

  • 當前作用中(Active):activeTopicId

  • 初始化標記:topicsInit

詳細指南

  • Action 模式:references/action-patterns.md
  • Slice 組織架構:references/slice-organization.md

類別化 Action 實現 (Class-Based Action Implementation)

我們正在將 slice 從純 StateCreator 物件遷移至基於類別的 action(class-based actions)

模式規範

  • 定義一個封裝 action 並且在建構函式接收 (set, get, api) 的類別。
  • 使用 #private 私有欄位(例如 #set#get)避免內部細節外洩。
  • 優先使用共用的型別輔助工具:
    • 來自 @/store/typesStoreSetter<T>(用於 set)。
    • Pick<ActionImpl, keyof ActionImpl>(僅暴露公開方法)。
  • 匯出回傳類別實例的 create*Slice 輔助函式。
type Setter = StoreSetter<HomeStore>;
export const createRecentSlice = (set: Setter, get: () => HomeStore, _api?: unknown) =>
  new RecentActionImpl(set, get, _api);

export class RecentActionImpl {
  readonly #get: () => HomeStore;
  readonly #set: Setter;

  constructor(set: Setter, get: () => HomeStore, _api?: unknown) {
    void _api;
    this.#set = set;
    this.#get = get;
  }

  useFetchRecentTopics = () => {
    // ...
  };
}

export type RecentAction = Pick<RecentActionImpl, keyof RecentActionImpl>;

組合模式 (Composition)

  • 在 store 檔案中,請使用 flattenActions 組合類別實例(切勿對類別實例使用展開運算子 ...)。
  • flattenActions 會將方法綁定至原始類別實例,並支援原型方法(prototype methods)與類別欄位(class fields)。
const createStore: StateCreator<HomeStore, [['zustand/devtools', never]]> = (...params) => ({
  ...initialState,
  ...flattenActions<HomeStoreAction>([
    createRecentSlice(...params),
    createHomeInputSlice(...params),
  ]),
});

多類別 Slices (Multi-Class Slices)

  • 對於需要多個 action 類別的大型 slice,請在 slice 入口處使用 flattenActions 進行組合。
  • 若需要組合多個類別並隱藏私有欄位,可以使用區域的 PublicActions<T> 輔助型別。
type PublicActions<T> = { [K in keyof T]: T[K] };

export type ChatGroupAction = PublicActions<
  ChatGroupInternalAction & ChatGroupLifecycleAction & ChatGroupMemberAction & ChatGroupCurdAction
>;

export const chatGroupAction: StateCreator<
  ChatGroupStore,
  [['zustand/devtools', never]],
  [],
  ChatGroupAction
> = (...params) =>
  flattenActions<ChatGroupAction>([
    new ChatGroupInternalAction(...params),
    new ChatGroupLifecycleAction(...params),
    new ChatGroupMemberAction(...params),
    new ChatGroupCurdAction(...params),
  ]);

Store 存取型別 (Store-Access Types)

  • 當類別方法依賴其他類別中的 action 時,請定義明確的 store 擴充型別(augmentation):
    • 用於生命週期 switchTopicChatGroupStoreWithSwitchTopic
    • 用於成員重新整理的 ChatGroupStoreWithRefresh
    • 用於 CRUD internal_dispatchChatGroupChatGroupStoreWithInternal

目前不需要 set 的 Slices

當 slice 不寫入本地狀態時(例如委派給另一個 store 或僅執行 hook),可移除 #set 並將建構函式參數標記為 _set 加上 void _set,以保持 (set, get, api) 的簽名結構:

export class ToolActionImpl {
  readonly #get: () => ConversationStore;

  constructor(_set: Setter, get: () => ConversationStore, _api?: unknown) {
    void _set;
    void _api;
    this.#get = get;
  }

  approveToolCall = async (id: string) => {
    const { context, hooks } = this.#get();
    await useChatStore.getState().approveToolCalling(id, '', context);
    hooks.onToolCallComplete?.(id, undefined);
  };
}
  • 未使用時請移除 #set;日後修改若需要 set 時再加回即可——重新新增無額外開銷。
  • 不要為不寫入狀態的 slice 新增 setNamespace
  • 在遷移過程中,請勿同時啟用舊版 slice 物件與類別 action。