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 me:
Hey, my name is Ramu Narasinga. I study large open-source projects and create content about their codebase architecture and best practices, sharing it through articles, videos.
I am open to work on an interesting project. Send me an email at ramu.narasinga@gmail.com
My Github - https://github.com/ramu-narasinga
My website - https://ramunarasinga.com
My Youtube channel - https://www.youtube.com/@thinkthroo
Learning platform - https://thinkthroo.com
Codebase Architecture - https://app.thinkthroo.com/architecture
Best practices - https://app.thinkthroo.com/best-practices
Production-grade projects - https://app.thinkthroo.com/production-grade-projects
References:
https://github.com/lobehub/lobe-chat/blob/main/src/store/session/store.ts
https://redux.js.org/style-guide/#use-the-redux-devtools-extension-for-debugging
https://zustand.docs.pmnd.rs/getting-started/introduction#first-create-a-store
https://github.com/lobehub/lobe-chat/blob/main/src/store/middleware/createDevtools.ts#L6
Top comments (0)