How to create slices with immer and auto type inference for slice actions? #3056
-
Hello zustand community! I've already found a suitable way to organize my zustand store especially with immer and combine to infer the types of my initial state and the store actions. My goal is not to have to explicitly create the types for the store actions or some computed get() selectors. So combine was the solution to realize it. But now I wanna separate this store in some slices and I can't figure out how, because of TypeScript errors which occur through the usage of immer in common and combine in the slice files. Could please somebody help me with these typescript issues? Here is a TS Playground of this issue. The current store without slices:useExampleStore.ts interface ExampleState {
header: {
title: string;
subtitle: string | undefined;
};
}
const initialState: ExampleState = {
header: {
title: "",
subtitle: undefined
}
};
const useExampleStore = create(
devtools(
subscribeWithSelector(
immer(
combine(initialState, (set) => ({
setHeaderTitle: (title: string) =>
set(
(state) => {
state.header.title = title;
},
undefined,
"Set header title"
),
setHeaderSubtitle: (subtitle: string) =>
set(
(state) => {
state.header.subtitle = subtitle;
},
undefined,
"Set header subtitle"
)
}))
)
),
{ store: "ExampleStore" }
)
); My slices approach with an example slice looks like this:useBoundStore.ts const useBoundStore = create(
devtools(
subscribeWithSelector(
immer((...args) => ({
...createExampleSlice(...args)
}))
),
{ store: "ExampleStore" }
)
); exampleSlice.ts interface ExampleState {
header: {
title: string;
subtitle: string | undefined;
};
}
const initialState: ExampleState = {
header: {
title: "",
subtitle: undefined
}
};
const createExampleSlice = combine(initialState, (set) => ({
setHeaderTitle: (title: string) =>
set((state) => {
state.header.title = title;
}),
setHeaderSubtitle: (subtitle: string) =>
set((state) => {
state.header.subtitle = subtitle;
})
})); The ts errors:In the useBoundStore.ts file I get the following ts error at Argument of type '() => unknown' is not assignable to parameter of type '() => Write<ExampleState, { setHeaderTitle: (title: string) => void; setHeaderSubtitle: (subtitle: string) => void; }>'.
Type 'unknown' is not assignable to type 'Write<ExampleState, { setHeaderTitle: (title: string) => void; setHeaderSubtitle: (subtitle: string) => void; }>'.
Type 'unknown' is not assignable to type 'Omit<ExampleState, "setHeaderTitle" | "setHeaderSubtitle">'.ts(2345)
(parameter) args: [setState: {
(nextStateOrUpdater: unknown, shouldReplace?: false, action?: Action | undefined): void;
(nextStateOrUpdater: unknown, shouldReplace: true, action?: Action | undefined): void;
}, getState: () => unknown, store: WithImmer<...>] In the exampleSlice.ts file I get the following ts error in both Argument of type '(state: ExampleState) => void' is not assignable to parameter of type 'ExampleState | Partial<ExampleState> | ((state: ExampleState) => ExampleState | Partial<ExampleState>)'.
Type '(state: ExampleState) => void' is not assignable to type '(state: ExampleState) => ExampleState | Partial<ExampleState>'.
Type 'void' is not assignable to type 'ExampleState | Partial<ExampleState>'.ts(2345) |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
I don't think that middleware is allowed in slices. I made zustand-namespaces to allow this sort of stuff. Or if you didnt want to use that, you would have to use the combine in the root of the store, not the slice. |
Beta Was this translation helpful? Give feedback.
-
Heyho! Thank you very much for your input @mooalot I just solved the problem without the combine middleware and published an own middleware for this solution: https://github.com/robertluedke/zustand-create-slices With this middleware I get a well structured store with store slices. Each slice has its own namespace and I do not need to explicit create the types for the actions, just for the pure data state. Bonus: I did add an automatic action name inference for the developer tools in the format "slice name/function name" like "bear/addBear". (which works with immer middleware and in the Firefox!! Both are issues right now in Zustand itself..) Now I am totally happy to have this easy api to create slices in Zustand! 😄 Look into the following example implementation: // useStore.ts
import { create, type StateCreator } from "zustand";
import { createSlices } from "zustand-create-slices";
import { devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
import { type BearSlice, createBearSlice } from "./bear.store";
import { createFishSlice, type FishSlice } from "./fish.store";
export type ExampleStore = {
bear: BearSlice;
fish: FishSlice;
};
type StoreSlice<T> = StateCreator<
ExampleStore,
[["zustand/devtools", never], ["zustand/immer", never]],
[],
T
>;
export type SliceSet<T> = Parameters<StoreSlice<T>>[0];
export type SliceGet<T> = Parameters<StoreSlice<T>>[1];
export const useStore = create<ExampleStore>()(
devtools(
immer(
createSlices({
bear: createBearSlice,
fish: createFishSlice
})
),
{ name: "Example Store" }
)
); // bear.store.ts
import type { SliceSet, SliceGet } from "./useStore";
interface BearSliceState {
bears: number;
}
const initialState: BearSliceState = {
bears: 0
};
export const createBearSlice = (set: SliceSet<BearSliceState>, get: SliceGet<BearSliceState>) => ({
...initialState,
addBear: () =>
set((state) => {
state.bear.bears += 1; // Direct mutation with immer
}),
eatFish: () =>
set((state) => {
state.fish.fishes -= state.bear.bears; // Cross-slice updates
}),
getMultipleBears: (n: number) => get().bear.bears * n
});
// Export the inferred store slice type
export type BearSlice = ReturnType<typeof createBearSlice>; |
Beta Was this translation helpful? Give feedback.
Heyho! Thank you very much for your input @mooalot
I just solved the problem without the combine middleware and published an own middleware for this solution: https://github.com/robertluedke/zustand-create-slices
With this middleware I get a well structured store with store slices. Each slice has its own namespace and I do not need to explicit create the types for the actions, just for the pure data state.
Bonus: I did add an automatic action name inference for the developer tools in the format "slice name/function name" like "bear/addBear". (which works with immer middleware and in the Firefox!! Both are issues right now in Zustand itself..)
Now I am totally happy to have this easy api t…