LobeHub Zustand State Management
Action Type Hierarchy
1. Public Actions
Main interfaces for UI components:
- Naming: Verb form (
createTopic,sendMessage) - Responsibilities: Parameter validation, flow orchestration
2. Internal Actions (internal_*)
Core business logic implementation:
- Naming:
internal_prefix (internal_createTopic) - Responsibilities: Optimistic updates, service calls, error handling
- Should not be called directly by UI
3. Dispatch Methods (internal_dispatch*)
State update handlers:
- Naming:
internal_dispatch+ entity (internal_dispatchTopic) - Responsibilities: Calling reducers, updating store
When to Use Reducer vs Simple set
Use Reducer Pattern:
- Managing object lists/maps (
messagesMap,topicMaps) - Optimistic updates
- Complex state transitions
Use Simple set:
- Toggling booleans
- Updating simple values
- Setting single state fields
Optimistic Update Pattern
typescript1internal_createTopic: async (params) => { 2 const tmpId = Date.now().toString(); 3 4 // 1. Immediately update frontend (optimistic) 5 get().internal_dispatchTopic( 6 { type: 'addTopic', value: { ...params, id: tmpId } }, 7 'internal_createTopic' 8 ); 9 10 // 2. Call backend service 11 const topicId = await topicService.createTopic(params); 12 13 // 3. Refresh for consistency 14 await get().refreshTopic(); 15 return topicId; 16},
Delete operations: Don't use optimistic updates (destructive, complex recovery)
Naming Conventions
Actions:
- Public:
createTopic,sendMessage - Internal:
internal_createTopic,internal_updateMessageContent - Dispatch:
internal_dispatchTopic - Toggle:
internal_toggleMessageLoading
State:
- ID arrays:
messageLoadingIds,topicEditingIds - Maps:
topicMaps,messagesMap - Active:
activeTopicId - Init flags:
topicsInit
Detailed Guides
- Action patterns:
references/action-patterns.md - Slice organization:
references/slice-organization.md
Class-Based Action Implementation
We are migrating slices from plain StateCreator objects to class-based actions.
Pattern
- Define a class that encapsulates actions and receives
(set, get, api)in the constructor. - Use
#privatefields (e.g.,#set,#get) to avoid leaking internals. - Prefer shared typing helpers:
StoreSetter<T>from@/store/typesforset.Pick<ActionImpl, keyof ActionImpl>to expose only public methods.
- Export a
create*Slicehelper that returns a class instance.
ts1type Setter = StoreSetter<HomeStore>; 2export const createRecentSlice = (set: Setter, get: () => HomeStore, _api?: unknown) => 3 new RecentActionImpl(set, get, _api); 4 5export class RecentActionImpl { 6 readonly #get: () => HomeStore; 7 readonly #set: Setter; 8 9 constructor(set: Setter, get: () => HomeStore, _api?: unknown) { 10 void _api; 11 this.#set = set; 12 this.#get = get; 13 } 14 15 useFetchRecentTopics = () => { 16 // ... 17 }; 18} 19 20export type RecentAction = Pick<RecentActionImpl, keyof RecentActionImpl>;
Composition
- In store files, merge class instances with
flattenActions(do not spread class instances). flattenActionsbinds methods to the original class instance and supports prototype methods and class fields.
ts1const createStore: StateCreator<HomeStore, [['zustand/devtools', never]]> = (...params) => ({ 2 ...initialState, 3 ...flattenActions<HomeStoreAction>([ 4 createRecentSlice(...params), 5 createHomeInputSlice(...params), 6 ]), 7});
Multi-Class Slices
- For large slices that need multiple action classes, compose them in the slice entry using
flattenActions. - Use a local
PublicActions<T>helper if you need to combine multiple classes and hide private fields.
ts1type PublicActions<T> = { [K in keyof T]: T[K] }; 2 3export type ChatGroupAction = PublicActions< 4 ChatGroupInternalAction & ChatGroupLifecycleAction & ChatGroupMemberAction & ChatGroupCurdAction 5>; 6 7export const chatGroupAction: StateCreator< 8 ChatGroupStore, 9 [['zustand/devtools', never]], 10 [], 11 ChatGroupAction 12> = (...params) => 13 flattenActions<ChatGroupAction>([ 14 new ChatGroupInternalAction(...params), 15 new ChatGroupLifecycleAction(...params), 16 new ChatGroupMemberAction(...params), 17 new ChatGroupCurdAction(...params), 18 ]);
Store-Access Types
- For class methods that depend on actions in other classes, define explicit store augmentations:
ChatGroupStoreWithSwitchTopicfor lifecycleswitchTopicChatGroupStoreWithRefreshfor member refreshChatGroupStoreWithInternalfor curdinternal_dispatchChatGroup
Do / Don't
- Do: keep constructor signature aligned with
StateCreatorparams(set, get, api). - Do: use
#privateto avoidset/getbeing exposed. - Do: use
flattenActionsinstead of spreading class instances. - Don't: keep both old slice objects and class actions active at the same time.