
Zustand
- 1.6k installs
- 81.3k repo stars
- Updated August 5, 2026
- lobehub/lobe-chat
This is a copy of zustand by lobehub - installs and ranking accrue to the original listing.
zustand is a LobeHub coding skill that enforces Zustand store conventions—including public, internal, and dispatch action layers—when creating or refactoring slices in AI chat or agent interfaces.
About
zustand is a LobeHub skill from lobehub/lobe-chat that documents Zustand store conventions for AI chat and agent UIs under src/store. It defines public, internal, and dispatch action layers, an optimistic update pattern, slice composition via flattenActions, and class-based action migration from StateCreator object slices to XxxActionImpl classes. Triggers include useChatStore, useUserStore, useGlobalStore, createStore, StoreSetter, internal_dispatch, messagesMap and topicsMap reducers, and debugging stale store reads. Developers reach for zustand when adding createXxxSlice functions, writing internal_* or internal_dispatch* actions, refactoring slices into class actions, or fixing selector staleness in LobeChat. The skill keeps multi-slice chat state consistent across topics and messages while separating public API actions from internal dispatch plumbing required for optimistic UI updates in conversational interfaces.
- Enforces public/internal/dispatch action hierarchy with strict naming rules
- Implements optimistic update pattern and slice composition via flattenActions
- Guides migration from StateCreator objects to class-based XxxActionImpl patterns
- Prevents stale store reads when working with messagesMap, topicsMap, or useChatStore selectors
- Triggers on any file under src/store/** involving createXxxSlice or internal_* actions
Zustand by the numbers
- 1,606 all-time installs (skills.sh)
- +83 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lobehub/lobe-chat --skill zustandAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 81.3k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | lobehub/lobe-chat ↗ |
How do LobeHub Zustand store slices handle actions?
Follow LobeHub conventions when creating or refactoring Zustand slices inside an AI chat or agent interface.
Who is it for?
Frontend developers working in lobehub/lobe-chat on useChatStore, slice actions, or optimistic message and topic state updates.
Skip if: Greenfield React apps without LobeHub conventions or projects using Redux or Jotai instead of LobeChat's Zustand patterns.
When should I use this skill?
User edits src/store, adds createXxxSlice, writes internal_dispatch, refactors to class actions, or debugs stale Zustand selectors.
What you get
Conformant Zustand slices with public/internal/dispatch layers, flattenActions composition, and optimistic update patterns in src/store.
- Conformant store slices
- Class or object action implementations
- Optimistic update action wiring
Files
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
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
#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.
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). flattenActionsbinds methods to the original class instance and supports prototype methods and class fields.
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.
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:
ChatGroupStoreWithSwitchTopicfor lifecycleswitchTopicChatGroupStoreWithRefreshfor member refreshChatGroupStoreWithInternalfor curdinternal_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:
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
#setwhen unused; restore it when a later edit needsset— re-adding costs nothing. - Don't add
setNamespacefor slices that don't write state. - Don't keep both old slice objects and class actions active at the same time during migration.
Zustand Action Patterns
Optimistic Update Implementation
Standard Flow
internal_updateMessageContent: async (id, content, extra) => {
const { internal_dispatchMessage, refreshMessages } = get();
// 1. Immediately update frontend
internal_dispatchMessage({
id,
type: 'updateMessage',
value: { content },
});
// 2. Call backend
await messageService.updateMessage(id, { content });
// 3. Refresh for consistency
await refreshMessages();
},Create Operations
internal_createMessage: async (message, context) => {
let tempId = context?.tempMessageId;
if (!tempId) {
tempId = internal_createTmpMessage(message);
}
try {
const id = await messageService.createMessage(message);
await refreshMessages();
return id;
} catch (e) {
internal_dispatchMessage({
id: tempId,
type: 'updateMessage',
value: { error: { type: ChatErrorType.CreateMessageError } },
});
}
},Delete Operations (No Optimistic Update)
internal_removeGenerationTopic: async (id: string) => {
get().internal_updateGenerationTopicLoading(id, true);
try {
await generationTopicService.deleteTopic(id);
await get().refreshGenerationTopics();
} finally {
get().internal_updateGenerationTopicLoading(id, false);
}
},Loading State Management
// Define in initialState.ts
export interface ChatMessageState {
messageEditingIds: string[];
}
// Manage in action
toggleMessageEditing: (id, editing) => {
set(
{ messageEditingIds: toggleBooleanList(get().messageEditingIds, id, editing) },
false,
'toggleMessageEditing',
);
};SWR Integration
useFetchMessages: (enable, sessionId, activeTopicId) =>
useClientDataSWR<ChatMessage[]>(
enable ? [SWR_USE_FETCH_MESSAGES, sessionId, activeTopicId] : null,
async ([, sessionId, topicId]) => messageService.getMessages(sessionId, topicId),
{
onSuccess: (messages) => {
const nextMap = { ...get().messagesMap, [messageMapKey(sessionId, activeTopicId)]: messages };
if (get().messagesInit && isEqual(nextMap, get().messagesMap)) return;
set({ messagesInit: true, messagesMap: nextMap }, false, n('useFetchMessages'));
},
}
),
// Cache invalidation
refreshMessages: async () => {
await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId]);
};Reducer Pattern
export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
switch (payload.type) {
case 'updateMessage': {
return produce(state, (draftState) => {
const index = draftState.findIndex((i) => i.id === payload.id);
if (index < 0) return;
draftState[index] = merge(draftState[index], {
...payload.value,
updatedAt: Date.now(),
});
});
}
// ...other cases
}
};Zustand Slice Organization
Top-Level Store Structure
Key aggregation files:
src/store/chat/initialState.ts: Aggregate all slice initial statessrc/store/chat/store.ts: Define top-levelChatStore, combine all slice actionssrc/store/chat/selectors.ts: Export all slice selectorssrc/store/chat/helpers.ts: Chat helper functions
Store Aggregation Pattern
// src/store/chat/initialState.ts
import { ChatTopicState, initialTopicState } from './slices/topic/initialState';
import { ChatMessageState, initialMessageState } from './slices/message/initialState';
export type ChatStoreState = ChatTopicState & ChatMessageState & ...
export const initialState: ChatStoreState = {
...initialMessageState,
...initialTopicState,
...
};
// src/store/chat/store.ts
export interface ChatStoreAction
extends ChatMessageAction, ChatTopicAction, ...
const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...params) => ({
...initialState,
...chatMessage(...params),
...chatTopic(...params),
});
export const useChatStore = createWithEqualityFn<ChatStore>()(
subscribeWithSelector(devtools(createStore)),
shallow
);Single Slice Structure
src/store/chat/slices/
└── [sliceName]/
├── action.ts # Define actions (or actions/ directory)
├── initialState.ts # State structure and initial values
├── reducer.ts # (Optional) Reducer pattern
├── selectors.ts # Define selectors
└── index.ts # (Optional) Re-exportsinitialState.ts
export interface ChatTopicState {
activeTopicId?: string;
topicMaps: Record<string, ChatTopic[]>;
topicsInit: boolean;
topicLoadingIds: string[];
}
export const initialTopicState: ChatTopicState = {
activeTopicId: undefined,
topicMaps: {},
topicsInit: false,
topicLoadingIds: [],
};selectors.ts
const currentTopics = (s: ChatStoreState): ChatTopic[] | undefined => s.topicMaps[s.activeId];
const getTopicById =
(id: string) =>
(s: ChatStoreState): ChatTopic | undefined =>
currentTopics(s)?.find((topic) => topic.id === id);
// Core pattern: Use xxxSelectors aggregate
export const topicSelectors = {
currentTopics,
getTopicById,
};Complex Actions Sub-directory
src/store/chat/slices/aiChat/
├── actions/
│ ├── generateAIChat.ts
│ ├── rag.ts
│ ├── memory.ts
│ └── index.ts
├── initialState.ts
└── selectors.tsState Design Patterns
Map Structure for Associated Data
topicMaps: Record<string, ChatTopic[]>;
messagesMap: Record<string, ChatMessage[]>;Arrays for Loading State
messageLoadingIds: string[]
topicLoadingIds: string[]Optional Fields for Active Items
activeId: string
activeTopicId?: stringBest Practices
1. Slice division: By functional domain (message, topic, aiChat) 2. File naming: camelCase for directories, consistent patterns 3. State structure: Flat, avoid deep nesting 4. Type safety: Clear TypeScript interfaces for each slice
Related skills
How it compares
Pick zustand over generic Zustand tutorials when contributing to lobehub/lobe-chat and must match its internal_dispatch and flattenActions conventions.
FAQ
What action layers does the LobeHub zustand skill define?
The LobeHub zustand skill separates public actions, internal actions, and dispatch actions, using internal_dispatch patterns and flattenActions for slice composition under src/store.
When should developers apply the zustand skill?
The zustand skill applies when working under src/store, adding createXxxSlice, writing internal_* actions, designing messagesMap or topicsMap reducers, or migrating slices to XxxActionImpl classes.
Is Zustand safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.