LobeHub Zustand Store Patterns
Install this skill
npx skills add lobehub/lobehubWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
What this skill does
- β’Class-based action encapsulation using private fields
- β’Strict separation between UI public actions and internal state dispatchers
- β’Helper utilities for flattening multi-class slices into a single store
- β’Optimistic update patterns for immediate UI responsiveness
- β’Strong typing support for complex state slices and action composition
When to use it
- βRefactoring large, unmanageable Zustand slices into modular classes
- βImplementing complex features requiring multiple service calls and optimistic UI updates
- βStandardizing state management across a multi-agent application team
- βBuilding high-performance chat features needing granular state access
When not to use it
- βSimple state requirements that only involve toggling boolean flags
- βSmall projects where plain function-based Zustand stores are sufficient
How to invoke it
Example prompts that trigger this skill:
- βImplement a new class-based action slice for the store following LobeHub patternsβ
- βConvert legacy object-based slice to a class-based structureβ
- βCreate a new internal dispatch method for a specific state entityβ
- βAdd an optimistic update flow within an existing internal action methodβ
- βCompose multiple action classes into a single store using flattenActionsβ
Example workflow
- Create a new class for the feature actions that accepts set and get in the constructor
- Define internal logic methods prefixed with 'internal_' for data handling
- Expose public action methods as class methods for UI consumption
- Define a slice creator function to instantiate the class with store parameters
- Merge the slice into the main store using the flattenActions helper
Prerequisites
- βZustand
- βTypeScript
- βFamiliarity with functional programming patterns in React
Pitfalls & limitations
- !Forgetting to use private class fields can leak internal store logic
- !Over-using optimistic updates for operations that require high data integrity
- !Complex slice compositions require careful management of store-access types
FAQ
How it compares
Unlike standard Zustand usage where logic is often colocated in one file, this pattern mandates a rigid architecture that scales better for large teams by strictly isolating the UI interface from core business logic.
Source & trust
From the source: β# 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 implementatiβ¦β
View the full SKILL.md source
# 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
```typescript
internal_createTopic: async (params) => {
const tmpId = Date.now().toString();
// 1. Immediately update frontend (optimistic)
get().internal_dispatchTopic(
{ type: 'addTopic', value: { ...params, id: tmpId } },
'internal_createTopic'
);
// 2. Call backend service
const topicId = await topicService.createTopic(params);
// 3. Refresh for consistency
await get().refreshTopic();
return topicId;
},
```
**Delete operations**: Don't use optimistic updates (destructive, complex recovery)
## Naming Conventions
**Actions:**
- Public: `createTopic`, `sendMessage`
- Internal: `internal_createTopic`, `internal_updateMessageContent`
- Dispatch: `internal_dispatchTopic`
**State:**
- ID arrays: `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 `#private` fields (e.g., `#set`, `#get`) to avoid leaking internals.
- Prefer shared typing helpers:
- `StoreSetter<T>` from `@/store/types` for `set`.
- `Pick<ActionImpl, keyof ActionImpl>` to expose only public methods.
- Export a `create*Slice` helper that returns a class instance.
```ts
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
- In store files, merge class instances with `flattenActions` (do not spread class instances).
- `flattenActions` binds methods to the original class instance and supports prototype methods and class fields.
```ts
const createStore: StateCreator<HomeStore, [['zustand/devtools', never]]> = (...params) => ({
...initialState,
...flattenActions<HomeStoreAction>([
createRecentSlice(...params),
createHomeInputSlice(...params),
]),
});
```
### 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.
```ts
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-Access Types
- For class methods that depend on actions in other classes, define explicit store augmentations:
- `ChatGroupStoreWithSwitchTopic` for lifecycle `switchTopic`
- `ChatGroupStoreWithRefresh` for member refresh
- `ChatGroupStoreWithInternal` for curd `internal_dispatchChatGroup`
### Slices That Don't Currently Need `set`
When a slice doesn't write local state (e.g. it delegates to another store or just runs hooks), drop `#set` and mark the constructor param as `_set` with `void _set` to keep the `(set, get, api)` shape:
```ts
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);
};
}
```
- Drop `#set` when unused; restore it when a later edit needs `set` β re-adding costs nothing.
- Don't add `setNamespace` for slices that don't write state.
- Don't keep both old slice objects and class actions active at the same time during migration.
Quoted from lobehub/lobehub for reference β see the original for the authoritative, latest version.
π Full skill instructions β original source: lobehub/lobehub
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/zustand/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/lobehub/lobehub/zustand/SKILL.md - Cursor:
~/.cursor/skills/lobehub/lobehub/zustand/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/lobehub/lobehub/zustand/SKILL.md
π Install with CLI:npx skills add lobehub/lobehub
