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 元件呼叫的主要介面:
- 命名:動詞形式(
createTopic、sendMessage) - 職責:參數驗證、流程編排
2. 內部 Action (internal_*)
核心業務邏輯的具體實現:
- 命名:使用
internal_前綴(internal_createTopic) - 職責:樂觀更新、服務呼叫、錯誤處理
- 注意:不應直接由 UI 呼叫
3. Dispatch 方法 (internal_dispatch*)
狀態更新的處理函式:
- 命名:
internal_dispatch+ 實體(internal_dispatchTopic) - 職責:呼叫 reducer、更新 store 狀態
何時使用 Reducer 與簡單 set
使用 Reducer 模式:
- 管理物件陣列 / Map(如
messagesMap、topicMaps) - 樂觀更新
- 複雜的狀態過渡與變更
使用簡單 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:
-
公開:
createTopic、sendMessage -
內部:
internal_createTopic、internal_updateMessageContent -
Dispatch:
internal_dispatchTopic
State(狀態):
-
ID 陣列:
topicEditingIds -
Map:
topicMaps、messagesMap -
當前作用中(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/types的StoreSetter<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):
- 用於生命週期
switchTopic的ChatGroupStoreWithSwitchTopic - 用於成員重新整理的
ChatGroupStoreWithRefresh - 用於 CRUD
internal_dispatchChatGroup的ChatGroupStoreWithInternal
- 用於生命週期
目前不需要 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。




