Remove extraneous text such as "diuh" from the Overview section to maintain professionalism and clarity.
Add a brief introduction at the start of the document to orient readers to the purpose and scope of the documentation.
Ensure consistent terminology throughout the document. For example, always refer to "slices" as "state slices" if that is the convention elsewhere in the codebase or documentation.
Clarify the relationship between stores, slices, actions, reducers, and selectors early in the document, possibly with a simple diagram to illustrate the architecture.
Use consistent formatting for code snippets and section headings. For example, ensure all code blocks specify the language (e.g., ts or tsx).
Add cross-references to related documentation or source files where appropriate, especially when mentioning concepts like "HyperStorage" or custom middleware.
Expand on best practices by including common pitfalls or anti-patterns to avoid when working with state management in LobeChat.
Consider adding a section on testing state logic, including how to test selectors and actions in isolation.
Encourage contributions to the documentation by adding a short note or link to contribution guidelines at the end.
Example improvements applied:
LobeChat uses Zustand as its core state management library, organizing application state into modular domains such as chat, session, user, global, agent, knowledge base, and tool. Each domain is implemented as an independent store, composed of state slices that encapsulate state, actions, reducers, and selectors. This approach supports maintainability, scalability, and clear separation of concerns across the codebase. Middleware enhances the stores with features like persistence, debugging, and efficient subscriptions.
Each store in LobeChat is constructed by aggregating an initial state and multiple state slice actions into a single store interface. State slices represent logical segments of the application's state and business logic, such as chat messages, user preferences, or session data. State slices declare their own initial state, actions (which may be synchronous or asynchronous), reducers, and selectors. These are merged to form the complete store for each domain, enabling modular development and easy extension of features. For example, the chat store aggregates slices for messages, threads, AI chat, topics, sharing, translation, TTS, built-in tools, plugins, and portals, each imported from separate modules (source).
The directory structure for a typical store is as follows (source):
src/store/<domain>
├── initialState.ts # Initial state for the store
├── selectors.ts # Selectors for accessing state
├── slices/ # Modular state slices (actions, reducers, selectors)
│ ├── <slice>/ # Slice for a specific feature
│ │ ├── action.ts # Actions for the slice
│ │ ├── selectors.ts # Selectors for the slice
│ │ └── index.ts # Entry for the slice
├── store.ts # Store creation and export
├── hooks/ # Custom React hooks for hydration/status
└── helpers.ts # Utility functions
Stores are created using Zustand's createWithEqualityFn, which enables shallow comparison for optimized re-renders. Middleware such as devtools, persist, and subscribeWithSelector are applied to enhance debugging, persistence, and efficient subscriptions (source). Custom middleware like createDevtools conditionally enables devtools based on environment and URL parameters (source).
Example: Creating and exporting a store hook
import { subscribeWithSelector } from 'zustand/middleware';
import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { StateCreator } from 'zustand/vanilla';
import { createDevtools } from '../middleware/createDevtools';
import { ChatStoreState, initialState } from './initialState';
import { ChatMessageAction, chatMessage } from './slices/message/action';
// ...other slice imports
export type ChatStore = ChatMessageAction & ChatStoreState; // plus other slices
const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...params) => ({
...initialState,
...chatMessage(...params),
// ...other slices
});
const devtools = createDevtools('chat');
export const useChatStore = createWithEqualityFn<ChatStore>()(
subscribeWithSelector(devtools(createStore)),
shallow,
);
Slices encapsulate a segment of state and its related actions and reducers. Actions describe interactive events, such as user interactions or network requests, and may be synchronous or asynchronous. Reducers are pure functions that update state based on actions.
Example: Defining a state slice with actions
import { StateCreator } from 'zustand';
export interface SessionActions {
useFetchSessions: () => SWRResponse<any>;
}
export const createSessionSlice: StateCreator<
SessionStore,
[['zustand/devtools', never]],
[],
SessionAction
> = (set, get) => ({
useFetchSessions: () => {
// ...logic for initializing sessions
},
// ...other actions
});
(source)
Selectors are pure functions that retrieve and derive data from the store. They encapsulate complex state selection logic, improving modularity, reusability, and maintainability. Selectors are defined in separate files and imported into components, allowing components to access processed state data without knowing the underlying structure (source).
Example: Selector definition and usage
// selectors.ts
const pluginList = (s: PluginStoreState) => [...s.pluginList, ...s.customPluginList];
const displayPluginList = (s: PluginStoreState) =>
pluginList(s).map((p) => ({
author: p.author,
avatar: p.meta?.avatar,
createAt: p.createAt,
desc: pluginHelpers.getPluginDesc(p.meta),
homepage: p.homepage,
identifier: p.identifier,
title: pluginHelpers.getPluginTitle(p.meta),
}));
// In a component
import { usePluginStore } from '@/store/plugin';
import { pluginSelectors } from '@/store/plugin/selectors';
const Render = () => {
const list = usePluginStore(pluginSelectors.displayPluginList);
return <> ... </>;
};
Stores are enhanced with middleware for debugging (devtools), persistence (persist), and efficient subscriptions (subscribeWithSelector). Custom middleware such as createDevtools enables devtools conditionally based on environment and URL parameters (source). Persistence is handled via middleware and custom solutions like HyperStorage, which abstracts over localStorage, IndexedDB, and URL-based state sharing.
State is accessed and updated through exported hooks such as useChatStore, useSessionStore, useUserStore, useGlobalStore, useKnowledgeBaseStore, and useToolStore. These hooks are created with Zustand's createWithEqualityFn and shallow comparison for optimized re-renders. Components consume state data by importing the relevant store hook and selectors.
Example:
import { useChatStore } from '@/store/chat';
import { chatSelectors } from '@/store/chat/selectors';
const ChatComponent = () => {
const messages = useChatStore(chatSelectors.messageList);
// ...render messages
};
LobeChat's state management emphasizes modularity, maintainability, and scalability. State slices support separation of concerns, selectors encapsulate data logic, and middleware enhances development and user experience. The fractal architecture and clear directory structure make onboarding and feature extension straightforward. All concepts and patterns are documented to support contributors and maintainers (source).
For further improvements, consider adding a documentation style guide, establishing a regular review process, and expanding sections on testing and common pitfalls. Contributions to documentation are welcome; see the project's contribution guidelines for details.