# Client-Side State Management in NextChat: Zustand-Style Stores Explained

> Explore how NextChat manages client-side state with Zustand-style stores. Learn about custom utilities for IndexedDB persistence and versioned migrations.

- Repository: [NextChat/NextChat](https://github.com/ChatGPTNextWeb/NextChat)
- Tags: internals
- Published: 2026-02-28

---

**NextChat manages all UI state through independent, Zustand-style stores wrapped by a custom `createPersistStore` utility that automatically persists data to IndexedDB and handles versioned migrations.**

NextChat (ChatGPTNextWeb/NextChat) implements a modular client-side state architecture using a thin abstraction over Zustand. The codebase splits application state into domain-specific stores—such as chat sessions, configuration, and masks—each backed by automatic IndexedDB persistence. This approach provides type-safe, reactive state access across components while ensuring user data survives browser refreshes.

## The Core Architecture: createPersistStore

All state containers in NextChat are built using a shared wrapper defined in [`app/utils/store.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/utils/store.ts). This function standardizes how Zustand stores are created, configured, and persisted.

### The Wrapper Implementation

The `createPersistStore` function accepts three arguments: initial state, business methods, and persistence options. It composes Zustand's `persist` and `combine` middlewares to return a hook with integrated storage capabilities.

```typescript
// app/utils/store.ts
export function createPersistStore<T extends object, M>(
  state: T,
  methods: (set: SetStoreState<T & MakeUpdater<T>>, get: () => T & MakeUpdater<T>) => M,
  persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
) {
  persistOptions.storage = createJSONStorage(() => indexedDBStorage);
  
  return create(
    persist(
      combine({ ...state, lastUpdateTime: 0, _hasHydrated: false }, (set, get) => ({
        ...methods(set, get as any),
        markUpdate() { set({ lastUpdateTime: Date.now() }); },
        update(updater) {
          const copy = deepClone(get());
          updater(copy);
          set({ ...copy, lastUpdateTime: Date.now() });
        },
        setHasHydrated(v) { set({ _hasHydrated: v }); },
      })),
      persistOptions,
    ),
  );
}

```

The wrapper injects three universal helpers into every store: `markUpdate` for timestamp tracking, `update` for immutable state patches, and `setHasHydrated` to signal when IndexedDB rehydration completes.

### Persistence Strategy

By default, all stores use **IndexedDB** via `createJSONStorage(() => indexedDBStorage)`. This enables large-state storage (chat histories, image data) that exceeds localStorage quotas. The `onRehydrateStorage` callback flips the `_hasHydrated` flag, allowing UI components to defer rendering until persisted state is restored.

## Domain-Specific Store Slices

NextChat partitions state into nine independent stores, each defined in `app/store/` and focused on a single functional domain.

### Chat Sessions (useChatStore)

The chat store manages conversation history, message threads, and session metadata. Defined in [`app/store/chat.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/chat.ts), it exports `useChatStore` with methods like `newSession`, `forkSession`, and `onUserInput`.

```typescript
// app/store/chat.ts
const DEFAULT_CHAT_STATE = {
  sessions: [createEmptySession()],
  currentSessionIndex: 0,
  lastInput: "",
};

export const useChatStore = createPersistStore(
  DEFAULT_CHAT_STATE,
  (set, get) => ({
    currentSession() {
      return get().sessions[get().currentSessionIndex];
    },
    newSession(mask?: Mask) {
      // creates session logic
    },
    deleteSession(index: number) {
      // deletion logic
    },
  }),
  {
    name: StoreKey.Chat,
    version: 3.3,
    migrate(persistedState, version) {
      // schema migration from older versions
    },
  },
);

```

### Global Configuration (useAppConfig)

Application settings—theme, language, model defaults—reside in `useAppConfig` from [`app/store/config.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/config.ts). This store provides `setState` for partial updates and persists user preferences across sessions.

### Specialized Stores

- **Masks** (`useMaskStore` in [`app/store/mask.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/mask.ts)): Prompt templates and character presets.
- **Access Control** (`useAccessStore` in [`app/store/access.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/access.ts)): API keys, server URLs, and authentication state.
- **Cloud Sync** (`useSyncStore` in [`app/store/sync.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/sync.ts)): Backup/restore logic for cross-device synchronization.
- **Stable Diffusion** (`useSdStore` in [`app/store/sd.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/sd.ts)): Image generation parameters and UI state.
- **Plugins** (`usePluginStore` in [`app/store/plugin.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/plugin.ts)): Third-party tool integrations.
- **Prompt Manager** (`usePromptStore` in [`app/store/prompt.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/prompt.ts)): User-defined prompt libraries.
- **Updates** (`useUpdateStore` in [`app/store/update.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/update.ts)): Version checking and release notification state.

## Consuming State in React Components

Components interact with stores through two primary patterns: reactive selectors for reading state and direct method access for mutations.

### Selecting State with Hooks

Use the store hook with a selector function to subscribe to specific slices. This prevents unnecessary re-renders when unrelated state changes.

```tsx
// app/components/sidebar.tsx
import { useChatStore } from "@/app/store";

export function Sidebar() {
  const sessions = useChatStore(state => state.sessions);
  const currentSession = useChatStore(state => state.currentSession());

  return (
    <div>
      {sessions.map((session) => (
        <div key={session.id} className={session.id === currentSession?.id ? "active" : ""}>
          {session.topic}
        </div>
      ))}
    </div>
  );
}

```

### Calling Actions Outside React

For event handlers or non-React logic, access the store's methods directly via `getState()`. This bypasses the React render cycle and executes immediately.

```tsx
const handleCreateSession = () => {
  const { newSession } = useChatStore.getState();
  newSession(); // immediately creates a new chat session
};

```

## Persistence and Hydration Lifecycle

NextChat's state layer guarantees data durability through a structured hydration pipeline.

### IndexedDB Integration

When the application initializes, Zustand's `persist` middleware reads serialized state from IndexedDB and restores it to each store. The process runs asynchronously; components can check `useChatStore.getState()._hasHydrated` to determine if data is ready.

### Versioned Migrations

Each store declares a `version` number and an optional `migrate` function. When the stored schema version lags behind the application version, the migration function transforms legacy data shapes to match current expectations.

```typescript
// Example from app/store/chat.ts
migrate(persistedState, version) {
  if (version < 3.3) {
    // Convert old numeric IDs to nanoid strings
    persistedState.sessions.forEach(session => {
      if (typeof session.id === 'number') {
        session.id = nanoid();
      }
    });
  }
  return persistedState;
}

```

## Summary

- NextChat uses a custom `createPersistStore` wrapper in [`app/utils/store.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/utils/store.ts) to standardize Zustand store creation with IndexedDB persistence.
- State is partitioned into nine domain-specific stores (`useChatStore`, `useAppConfig`, `useMaskStore`, etc.), each handling a distinct UI concern.
- Components read state via selector hooks and mutate state through methods accessed via `useStore.getState()`.
- Automatic rehydration from IndexedDB is tracked via the `_hasHydrated` flag, ensuring UI consistency on startup.
- Versioned migration functions in each store handle schema evolution without data loss.

## Frequently Asked Questions

### How does NextChat persist state across page reloads?

NextChat uses Zustand's `persist` middleware configured with `createJSONStorage(() => indexedDBStorage)` to serialize store contents to IndexedDB. When the browser reloads, the `onRehydrateStorage` callback restores the saved state and sets `_hasHydrated` to true, making the data available to components immediately upon initialization.

### What is the difference between useChatStore and useAppConfig?

`useChatStore` (defined in [`app/store/chat.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/chat.ts)) manages transient conversation data—messages, sessions, and input history—while `useAppConfig` (in [`app/store/config.ts`](https://github.com/ChatGPTNextWeb/NextChat/blob/main/app/store/config.ts)) controls global application settings like theme, language, and default model parameters. Both use the same `createPersistStore` wrapper but maintain separate IndexedDB entries to isolate chat data from configuration.

### How does NextChat handle state migrations when the schema changes?

Each store declares a `version` number and an optional `migrate` function within its persistence options. When the application loads, Zustand compares the stored version against the current version; if they differ, it executes the store-specific migration logic to transform legacy data structures before the state becomes available to the UI.

### Can I access store methods outside of React components?

Yes. NextChat exposes store methods via `useStore.getState()`, which returns the raw store object containing all state and actions. This pattern is used throughout the codebase to trigger state changes from utility functions, event listeners, or asynchronous callbacks without needing to invoke a React hook.