Guide
State management in Lobechat

State management in Lobechat

State management is intertwined with the way you manage API layer. In this guide, we will analyze how the state is managed in Lobechat.

We have to take a closer look at operations such as:

  1. Add a new assistant
  2. Fetching the list of assistant
  3. Adding a message to a chat
  4. Loading a chat

These operations are specific to Lobechat, these operations vary based on an application. In cal.com, to understand how how state is managed, you would look at operations such as bookings list, event-types etc.,

Adding a new assistant:

Search for "New Assistant" button in the source code, catch here is that Lobechat has internationalization which means we need to find the key for "New Assistant", it is "newAgent".

Search again across the codebase for "newAgent" and you will find it in this file

How is newAgent variable used?

<ActionIcon
  icon={MessageSquarePlus}
  loading={isValidating}
  onClick={() => mutate()}
  size={DESKTOP_HEADER_ICON_SIZE}
  style={{ flex: 'none' }}
  title={t('newAgent')}
/>

In the above code, newAgent is used as title which is a prop in ActionIcon. This is it, this is our entry point that leads us to the state management. onClick calls a function named mutate.

const { mutate, isValidating } = useActionSWR('session.createSession', () => createSession());

Our focus is only on state management, checkout API Layer in LobeChat to learn more about useActionSWR, mutate.

Where is createSession coming from?

const [createSession] = useSessionStore((s) => [s.createSession]);

This path is now leading us to discover where useSessionStore is imported from.

import { useSessionStore } from '@/store/session';

useSessionStore is imported from store/session.ts

[C:Session store] [L: file structure] For us to understand the Session store in its entirety, let's start with the file structure.

You will find what each file is used for.

The following file structure is from Lobechat's store/session

+-- store/session                    # Main session store
|   +-- slices                       # Contains slices for session management
|   |   +-- session                  # Handles individual session logic
|   |   |   +-- selectors            # Selectors for session data
|   |   |   |   +-- index.ts         # Exports session selectors
|   |   |   |   +-- list.test.ts     # Tests for session list selector
|   |   |   |   +-- list.ts          # Session list selector
|   |   |   |   +-- meta.test.ts     # Tests for session meta selector
|   |   |   |   +-- meta.ts          # Session meta selector
|   |   |   +-- action.test.ts       # Tests for session actions
|   |   |   +-- action.ts            # Session actions
|   |   |   +-- helpers.ts           # Helper functions for session
|   |   |   +-- initialState.ts      # Initial state for session
|   |   |   +-- reducers.test.ts     # Tests for session reducers
|   |   |   +-- reducers.ts          # Reducers for session
|   +-- sessionGroup                 # Manages session groups
|   |   +-- action.test.ts           # Tests for session group actions
|   |   +-- action.ts                # Session group actions
|   |   +-- initialState.ts          # Initial state for session group
|   |   +-- reducer.test.ts          # Tests for session group reducer
|   |   +-- reducer.ts               # Reducer for session group
|   |   +-- selectors.ts             # Selectors for session group
|   +-- store.ts                     # Main store configuration for sessions
+-- helpers.ts                       # Helper functions for the session store
+-- index.ts                         # Entry point for the session store
+-- initialState.ts                  # Initial state for the session store
+-- selectors.ts                     # Selectors for the session store
 

This is for session store, Lobechat has a lot of other folder inside store

[insert screenshot of lobechat store folder].

Standard file structure

It looks it follows consistent file structure across these store folders. a store folder will have the below file structure:

Store folder - Root level

+-- slices
+-- helpers.ts
+-- index.ts
+-- initialState.ts
+-- selectors.ts or selectors being a folder
+-- reducers 

slicers folder structure

+-- {slice-name} folder             # Here slice-name can be a store slice, for example, user store has auth, settings etc., 

Each slice folder then follow the same file structure convention found at the root level, except there is one additional files called reducers.ts and action.ts

[L: store/session explained] In this guide, we analyze the files in store/session to understand how it all comes together and being used in a page/UI. Let's begin.

index.ts

export type { SessionStore } from './store';
export { useSessionStore } from './store';

index.ts simply exports type and a hook named useSessionStore. This practice allows you to import useSessionStore by writing - @/store/session instead of @/store/session/store as useSessionStore is in store file.

store.ts

store.ts has some advanced Zustand API. This store.ts is a perfect demonstration of how an experienced senior dev would manage Zustand store.

Let's first analyze how the store is created.

const createStore: StateCreator<SessionStore, [['zustand/devtools', never]]> = (...parameters) => ({
  ...initialState,
  ...createSessionSlice(...parameters),
  ...createSessionGroupSlice(...parameters),
});
 
//  ===============  implement useStore ============ //
const devtools = createDevtools('session');
 
export const useSessionStore = createWithEqualityFn<SessionStore>()(
  subscribeWithSelector(
    devtools(createStore, {
      name: 'LobeChat_Session' + (isDev ? '_DEV' : ''),
    }),
  ),
  shallow,
);

You might be familiar with simple use case of Zustand but this example from Lobechat's session store shows you how you can leverage advanced API such as devTools, createWithEqualityFn, subscribeWithSelector.

We start off with the below code snippet:

const createStore: StateCreator<SessionStore, [['zustand/devtools', never]]> = (...parameters) => ({
  ...initialState,
  ...createSessionSlice(...parameters),
  ...createSessionGroupSlice(...parameters),
});

StateCreator is a type imported from zustand/vanilla. Lobechat uses slices pattern. In fact, what you see in the above code snippet - StateCreator<SessionStore, [['zustand/devtools', never]]> is mentioned in Zustand documentation. Check this full list of Middlewares and their mutators reference. It is also worth reading Zustand's TypeScript guide.

This createStore has initialState, createSessionSlice and createSessionGroupSlice. initialState is imported from a file named initialState.ts. createSessionSlice is imported from session/action.ts. createSessionGroupSlice is imported from sessionGroup/action.ts

Let's now examine the devtools middleware

const devtools = createDevtools('session');

devtools middleware lets you use Redux DevTools Extension without Redux. Read more about the benefits of using Redux DevTools for debugging.

In the code snippet below, you will find how the devTools and createStore are used to create useSessionStore.

export const useSessionStore = createWithEqualityFn<SessionStore>()(
  subscribeWithSelector(
    devtools(createStore, {
      name: 'LobeChat_Session' + (isDev ? '_DEV' : ''),
    }),
  ),
  shallow,
);

You will find that createWithEqualityFn is called with subscribeWithSelector. subscribeWithSelector has devTools as its only param. devtools is a middleware with its first parameter being createStore function.

createWithEqualityFn createWithEqualityFn lets you create a React Hook with API utilities attached, just like create. However, it offers a way to define a custom equality check. This allows for more granular control over when components re-render, improving performance and responsiveness.

subscribeWithSelector

[L: Slices in zustand] In this guide, we discuss slices pattern in Zustand You would need to understand slice in Zustand before we study state management in Lobechat

References:

  1. https://github.com/lobehub/lobe-chat/blob/7d1e5c46aecc582308483d329b6007a8f8c76b70/src/app/(main)/chat/%40session/_layout/Desktop/SessionHeader.tsx#L36
  2. https://chat-preview.lobehub.com/chat?session=inbox
  3. https://github.com/lobehub/lobe-chat/tree/7d1e5c46aecc582308483d329b6007a8f8c76b70/src/store/session