
Zustand
Follow LobeChat’s Zustand conventions when adding store slices, internal actions, and optimistic updates under src/store without breaking UI versus dispatch boundaries.
Overview
Zustand is an agent skill for the Build phase that enforces LobeChat store conventions—public, internal, and dispatch layers—for Zustand slices and optimistic updates.
Install
npx skills add https://github.com/lobehub/lobe-chat --skill zustandWhat is this skill?
- Three-layer action model: public verbs for UI, internal_* business logic, internal_dispatch* reducers
- Guidance on reducer pattern for maps/lists like messagesMap and topicsMap versus simple set updates
- Slice composition via flattenActions and migration path from StateCreator objects to XxxActionImpl classes
- Optimistic update flow wired through internal actions rather than direct UI calls to dispatch
- Explicit triggers for useChatStore, useUserStore, useGlobalStore, and createStore patterns
- Documents a 3-layer action hierarchy: public, internal_*, and internal_dispatch*
Adoption & trust: 878 installs on skills.sh; 78.4k GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You are patching LobeChat store code and keep mixing UI calls, business logic, and reducers in ways that cause stale selectors or duplicated optimistic flows.
Who is it for?
Contributors or forkers working under LobeChat src/store who add features to chat, topics, or global user state.
Skip if: Greenfield React apps with no LobeChat codebase unless you intentionally adopt the same internal_/dispatch naming scheme.
When should I use this skill?
Working under src/store, adding slices or actions, using useChatStore/useUserStore/useGlobalStore, flattenActions, StoreSetter, internal_dispatch, class action migration, or debugging optimistic updates.
What do I get? / Deliverables
New slices and actions follow LobeHub’s layered pattern so components call only public actions while internal_* and internal_dispatch* own updates consistently.
- Store slices and actions that match LobeHub layering
- Reducer or set updates chosen correctly for map/list state
Recommended Skills
Journey fit
How it compares
Project-specific Zustand conventions—not the minimal generic create() tutorial from the Zustand docs alone.
Common Questions / FAQ
Who is zustand for?
Developers extending LobeHub LobeChat who need the project’s Zustand slice, internal action, and optimistic-update rules while editing src/store.
When should I use zustand?
During Build frontend work when you add createXxxSlice, refactor class-based actions, fix selector staleness, or wire messagesMap/topicsMap reducers.
Is zustand safe to install?
It guides code structure only; safety depends on the upstream LobeChat skill repo—check the Security Audits panel on this Prism page before install.
SKILL.md
READMESKILL.md - Zustand
# 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<HomeS