How to configure DevTools for your Zustand store?

In this article, you will learn how to configure DevTools for your Zustand store. We will use the Lobechat source code and Zustand documentation as our reference.

Debugging a store

In the docs, Debugging a store provides this below code as an example:

import { create, StateCreator } from 'zustand'
import { devtools } from 'zustand/middleware'

type JungleStore = {
  bears: number
  addBear: () => void
  fishes: number
  addFish: () => void
}

const useJungleStore = create<JungleStore>()(
 devtools((…args) => ({
   bears: 0,
   addBear: () =>
     set((state) => ({ bears: state.bears + 1 }), undefined, 'jungle/addBear'),
   fishes: 0,
   addFish: () => set(
     (state) => ({ fishes: state.fishes + 1 }),
     undefined,
     'jungle/addFish',
     ),
   })),
)

The question to ask here is how is this different from the simple createStore API without DevTools. In the Create a store documentation,
you will see this below code snippet:

import { create } from 'zustand'
const useStore = create((set) => ({
 bears: 0,
 increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
 removeAllBears: () => set({ bears: 0 }),
 updateBears: (newBears) => set({ bears: newBears }),
}))

Well, can you tell the difference between these two?

1. Debug example has better types.

type JungleStore = {
 bears: number
 addBear: () => void
 fishes: number
 addFish: () => void
}
const useJungleStore = create<JungleStore>()(

2. The function passed to create is wrapped over by devTools function in the Debug example.

// Debugging enabled
const useJungleStore = create<JungleStore>()(
 devtools((…args) => ({
// Without debugging
const useStore = create((set) => ({
 bears: 0,

3. There are three parameters passed into the set function when the debug is enabled.

// Debugging enabled
addBear: () =>
 set((state) => ({ bears: state.bears + 1 }), undefined, 'jungle/addBear'),
// Without debugging
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),

It is easy to understand that the third parameter is the action name, but why is the second parameter above is defined as undefined. I found the below statements about this undefined parameter in the documentation.

Additionally, to preserve the default behavior of the replacement logic, the second parameter should be set to undefined.

Do not set the second parameter to true or false unless you want to override the
default replacement logic

Now that we understand the basics of configuring DevTools for a Zustand store. Let’s review the code in Lobechat Zustand store.

DevTools configuration in Lobechat

Lobechat’s state management is complex, but for simplicity purposes, let’s choose store/session/store.ts

devTools

The way devTools is implemented is different in LobeChat.

const devtools = createDevtools('session');

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

Instead of directly importing devtools from zustand/middleware. createDevTools is a function imported from createDevTools.ts and has the below code:

import { optionalDevtools } from 'zustand-utils';
import { devtools as _devtools } from 'zustand/middleware';

import { isDev } from '@/utils/env';

export const createDevtools =
  (name: string): typeof _devtools =>
  (initializer) => {
    let showDevtools = false;

    // check url to show devtools
    if (typeof window !== 'undefined') {
      const url = new URL(window.location.href);
      const debug = url.searchParams.get('debug');
      if (debug?.includes(name)) {
        showDevtools = true;
      }
    }

    return optionalDevtools(showDevtools)(initializer, {
      name: `LobeChat_${name}` + (isDev ? '_DEV' : ''),
    });
  };

This code tells us that if the url contains a query param named showDevtools and is true, only then show DevTools and uses optionalDevTools imported from zustand-utils.

Often times, concepts found in documentation are not implemented as is in the open-source, this above way of configuring debugging demonstrates that developers at LobeChat are extremely good at what they do.

Action labels in the DevTools

Even the action labels are done differently. At line 167 in session slice action, you will find this below code snippet:

switchSession: (sessionId) => {
 if (get().activeId === sessionId) return;
set({ activeId: sessionId }, false, n(`activeSession/${sessionId}`));
},

What’s the ’n’ here? Line 31 indicates its got something to do with Namespace.

const n = setNamespace('session');

This namespace concept deserves its own article and you can find it on our blog.

About us:

At Thinkthroo, we study large open source projects and provide architectural guides. We have developed reusable Components, built with tailwind, that you can use in your project.

We offer Next.js, React and Node development services.

Book a meeting with us to discuss your project.

References:

  1. https://github.com/lobehub/lobe-chat/blob/main/src/store/session/store.ts

  2. https://zustand.docs.pmnd.rs/middlewares/devtools

  3. https://redux.js.org/style-guide/#use-the-redux-devtools-extension-for-debugging

  4. https://zustand.docs.pmnd.rs/getting-started/introduction#first-create-a-store

  5. https://github.com/lobehub/lobe-chat/blob/main/src/store/middleware/createDevtools.ts#L6