Closed KeeganFargher closed 3 years ago
Something like this should work:
const useStore = create<
ReturnType<typeof createBearSlice> & ReturnType<typeof createFishSlice>
>((set, get) => ({
...createBearSlice(set, get),
...createFishSlice(set, get)
}));
Though a utility type might be a bit cleaner, especially if you use this a lot in your codebase:
type StateFromFunctions<T extends [...any]> = T extends [infer F, ...(infer R)]
? F extends (...args: any) => object
? StateFromFunctions<R> & ReturnType<F>
: unknown
: unknown;
type State = StateFromFunctions<[
typeof createBearSlice,
typeof createFishSlice
]>;
const useStore = create<State>((set, get) => ({
...createBearSlice(set, get),
...createFishSlice(set, get)
}));
Thanks for the response! How could I get type safety on the slices? As in the example:
const createBearSlice = (set, get) => ({
eatFish: () => set((prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0}))
})
How can I get type safety on fishes
if it's in a different slice?
Thanks!
You can declare an interface for each function, and merge them to get the final state type. You can also use GetState
and SetState
to type your functions' arguments.
See: https://codesandbox.io/s/nostalgic-voice-3knvd?file=/src/store/useStore.ts
Awesome, thanks so much for the help!
I'm not completely satisfied with this solution because I would like to enforce that each slice won't mess with other slices' state.
I propose this solution instead:
import create, {GetState, SetState, StateCreator, StoreApi} from 'zustand';
import produce from 'immer';
interface IAuthSlice {
userToken?: string;
user?: IUser;
signIn: (payload: ISignIn) => Promise<void>;
}
const createAuthSlice: StateCreator<IAuthSlice> | StoreApi<IAuthSlice> = (
set,
get,
) => ({
isLoading: true,
userToken: undefined,
user: undefined,
signIn: async (payload) => {
const {access_token, user} = await signIn(payload);
set(state => {
...state,
state.user = user;
state.isLoading = false;
state.userToken = access_token;
state.preferences = [...]; // ERROR: don't mess with other slices' state!
}),
},
});
interface IPreferencesSlice {
preferences?: IPreferences;
}
const createPreferencesSlice: StateCreator<IPreferencesSlice> | StoreApi<IPreferencesSlice> = () => ({
[...]
});
interface IStore extends IAuthSlice, IPreferencesSlice {}
export const useStore = create<IStore>((set, get, api) => ({
...createAuthSlice(
set as unknown as SetState<IAuthSlice>,
get as GetState<IAuthSlice>,
api as unknown as StoreApi<IAuthSlice>,
),
...createPreferencesSlice(
set as unknown as SetState<IPreferencesSlice>,
get as GetState<IPreferencesSlice>,
api as unknown as StoreApi<IPreferencesSlice>,
),
}));
This has also the advantage that type checks the return value from createAuthSlice
and IPreferencesSlice
and complains if a required property is missing from each slice.
To add to @darkbasic's answer, you could also create a utility type to avoid repeating too much the slice's type:
type StateSlice<T extends object> = StateCreator<T> | StoreApi<T>;
Hello! i have build this example from darkbasic on sandbox: https://codesandbox.io/s/zustand-typescript-demo-kr182
in the createFishSlice.ts i have insert the type StateSlice<T extends object> = StateCreator<T> | StoreApi<T>;
but now i become in the useStore.ts this error:
This expression is not callable.
Not all constituents of type 'StateSlice
without the | StoreApi<T>;
is all fine!
I've made an utility type which fixes your issue:
// Each slice can only access its own state, plus a common part
export type SliceStateCreator<
S extends State,
C extends State = {}, // The common part accessible by the slice
T extends S = S & C,
CustomSetState = SetState<T>,
> = (set: CustomSetState, get: GetState<T>, api: StoreApi<T>) => S;
The example becomes:
store.ts
interface IStore extends IAuthSlice, IUserSlice {}
export const useStore = create<IStore>((set, get, api) => ({
...createAuthSlice(
set as unknown as SetState<IAuthSlice & IAuthCommon>,
get as unknown as GetState<IAuthSlice & IAuthCommon>,
api as unknown as StoreApi<IAuthSlice & IAuthCommon>,
),
...createUserSlice(
set as unknown as SetState<IUserSlice & IUserCommon>,
get as unknown as GetState<IUserSlice & IUserCommon>,
api as unknown as StoreApi<IUserSlice & IUserCommon>,
),
}));
auth-slice.ts
export type IAuthCommon = {
[...]
};
export type IAuthSlice = {
[...]
};
export const createAuthSlice: SliceStateCreator<IAuthSlice, IAuthCommon> = (
set,
get,
): IAuthSlice => ({
[...]
});
user-slice.ts
export type IUserCommon = {
[...]
};
export type IUserSlice = {
[...]
};
export const createUserSlice: SliceStateCreator<IUserSlice, IUserCommon> = (
set,
get,
): IUserSlice => ({
[...]
});
This approach has the additional advantage that you aren't forced to share the same common part of your state across all of your slices, but you can decide exactly which portion gets accessed by each and every slice.
thanks a lot! works fine...
i think the ICommon interface in the store.ts is not needed, because everything should actually be available in the xxx-Slice interfaces.
I have updates the sandbox code: https://codesandbox.io/s/zustand-typescript-demo-kr182 ...if anyone wants to see it in action! ;-)
😏
store.ts
import create, { SetState, GetState } from 'zustand'
import { FirstSlice, createFirstSlice } from 'first-slice.ts'
import { SecondSlice, createSecondSlice } from 'second-slice.ts'
export type StoreState = FirstSlice & SecondSlice
export type StoreSlice<T> = (
set: SetState<StoreState>,
get: GetState<StoreState>
) => T
const useStore = create<StoreState>((set, get) => ({
...createFirstSlice(set, get),
...createSecondSlice(set, get),
}))
first-slice.ts
export type FirstSlice = {}
export const createFirstSlice: StoreSlice<FirstSlice> = (set, get) => ({})
second-slice.ts
export type SecondSlice = {}
export const createSecondSlice: StoreSlice<SecondSlice> = (set, get) => ({})
I'm using @unsady version because is the only one i'm able to understand 😂 and it works as intended!
However, had to add
import { StoreSlice } from './store'
in both slices, which triggers a "Dependency cycle detected" ts error :S
FMHO that should be a warning and should be called "file dependency cycle" and only an error when there is a real variable dependency cycle.
Anyways, I've stored all the types in a file types.ts and that silenced the error
as per @unsady's and @cabanossi's progress and the middlewaretypes example managed to get this working with devtools, persist and immer:
useStore.ts
import { createSelectorHooks } from "auto-zustand-selectors-hook";
import produce, { Draft } from "immer";
import create, { GetState, State, StateCreator, StoreApi } from "zustand";
import { devtools, NamedSet, persist, StoreApiWithDevtools, StoreApiWithPersist } from "zustand/middleware";
import createGlobalSlice, { GlobalSlice } from "./slices/createGlobalSlice";
import createProfileSlice, { ProfileSlice } from "./slices/createProfileSlice";
const immer = <
T extends State,
CustomSetState extends NamedSet<T>,
CustomGetState extends GetState<T>,
CustomStoreApi extends StoreApi<T>
>(config: StateCreator<
T,
(partial: ((draft: Draft<T>) => void) | T, replace?: boolean, name?: string) => void,
CustomGetState,
CustomStoreApi
>): StateCreator<T, CustomSetState, CustomGetState, CustomStoreApi> => (set, get, api) =>
config((partial, replace, name) => {
const nextState =
typeof partial === 'function'
? produce(partial as (state: Draft<T>) => T)
: (partial as T)
return set(nextState, replace, name)
}, get, api);
export type IStore = GlobalSlice & ProfileSlice;
export type StoreSlice<T> = (
set: NamedSet<IStore>,
get: GetState<IStore>,
api: StoreApi<IStore>
) => T;
const useStore = createSelectorHooks(create<
IStore,
NamedSet<IStore>,
GetState<IStore>,
StoreApiWithPersist<IStore> & StoreApiWithDevtools<IStore>
>(
devtools(
persist(
immer((set, get, api) => ({
...createProfileSlice(set as NamedSet<IStore>, get, api),
...createGlobalSlice(set as NamedSet<IStore>, get, api),
})),
{
name: 'persisted-global-store',
getStorage: () => sessionStorage
}
),
{
name: 'global-store'
}
)
));
export default useStore;
createProfileSlice.ts
import { User } from "oidc-client";
import { initialProfileState } from "../initialStates";
import { StoreSlice } from "../useStore";
export interface ProfileSlice {
userProfile: User;
setUserProfile: (profile: User) => void;
isAuthenticated?: boolean;
}
const createProfileSlice: StoreSlice<ProfileSlice> = (set) => ({
userProfile: initialProfileState,
isAuthenticated: false,
setUserProfile: (profile: User) => {
if (profile)
set({ userProfile: profile, isAuthenticated: true }, false, "setUserProfile");
}
});
export default createProfileSlice;
createGlobalSlice.ts
import { StoreSlice } from "../useStore";
export interface GlobalSlice {
isGlobalLoading: boolean;
setIsGlobalLoading: (isLoading: boolean) => void;
}
const createGlobalSlice: StoreSlice<GlobalSlice> = (set) => ({
isGlobalLoading: false,
setIsGlobalLoading: (isLoading: boolean) => set({ isGlobalLoading: isLoading })
});
export default createGlobalSlice;
Maybe it helps another fellow dev. I don't really have in-depth knowledge of typescript so it was really hard to understand what's happening there.
If we merge both slices A and B, then when slice A changes, it will trigger re-render for both slice B. I think we should split both slice A and B into 2 different stores 🤩
// from
const useStore = create((set, get) => ({
...createSliceA(set, get),
...createSliceB(set, get)
}));
// into
const useStoreA = create((set, get) => createSliceA(set, get));
const useStoreB = create((set, get) => createSliceB(set, get));
If we merge both slices A and B, then when slice A changes, it will trigger re-render for both slice B. I think we should split both slice A and B into 2 different stores 🤩
// from const useStore = create((set, get) => ({ ...createSliceA(set, get), ...createSliceB(set, get) })); // into const useStoreA = create((set, get) => createSliceA(set, get)); const useStoreB = create((set, get) => createSliceB(set, get));
This won't work if there is a state that replies on each other. And please use TypeScript.
This won't work if there is a state that replies on each other. And please use TypeScript.
I've been to this thread a few times in the past, and found myself staring it in the face again today. I feel compelled to spot the elephant in the room here and suggest that all of the above circus act is good example of when the tools create more trouble than they're solving. That said, a couple of points to note:
I'm doing something like this:
utils/state.ts
import { UnionToIntersection } from "type-fest"; import create, { StateCreator, UseBoundStore } from "zustand"; import { devtools } from "zustand/middleware"; import { capitalize, uncapitalize } from "utils/tools"; import { AnyFn, NonEmptyArray, NonOptional, Optional, Str, StrKey } from "utils/type"; type State = Readonly<{ [Key in string]: any }>; type Key<TState extends State> = StrKey<TState>; type Keys<TState extends State> = NonEmptyArray<Key<TState>>; type StateDef<TState extends State, K extends Keys<TState> = Keys<TState>> = Readonly<{ builder: StateCreator<TState>; keys: K; }>; type Interface<TState extends State, Keys extends StrKey<TState>> = { [Key in Keys]: () => TState[Key]; }; type MapDef = Readonly<{ [Key in string]: StateDef<any> }>; type ReturnState<T extends StateDef<any>> = T extends StateDef<infer State> ? State : never; type UnionState<States extends MapDef, Keys extends StrKey<States> = StrKey<States>> = Readonly< UnionToIntersection< { [Prefix in Keys]: { [Key in StrKey<States[Prefix]["keys"][number]> as `${Prefix}${Capitalize<Key>}`]: ReturnState<States[Prefix]>[Key]; }; }[Keys] > >; const empty: AnyFn = () => { throw "Called empty function, please revise state definition"; }; // TODO: set with function function prefix<T extends State, Creator extends StateCreator<T> = StateCreator<T>>( prefix: string, keys: Keys<T>, creator: Creator, _set: Parameters<Creator>[0], _get: Parameters<Creator>[1], _: Parameters<Creator>[2], ) { const pre = <O extends State>(state: O) => Object.entries(state).reduce( (acc, [key, val]) => ({ ...acc, [`${prefix}${capitalize(key)}`]: val, }), {}, ); const set: Parameters<Creator>[0] = (state, replace) => { if (typeof state === "function") throw "Unimplemented"; return _set(pre(state), replace); }; const get = () => Object.entries(_get()) .filter(([key]) => keys.includes(key as Key<T>)) .reduce( (acc, [key, value]) => ({ ...acc, [uncapitalize(key.substring(prefix.length))]: value, }), {} as T, ); return pre(creator(set, get, _)); } function createInterface<TState extends State, Keys extends StrKey<TState>>( store: UseBoundStore<TState>, keys: NonEmptyArray<Keys>, prefix: string, ) { const functions = keys.reduce((acc, key) => { if (acc[key]) throw "Duplicated key in state"; const prefixed = prefix + capitalize(key); return { ...acc, [key]: (state: TState) => state[prefixed], }; }, {} as { [key in Keys]: (state: TState) => TState[key] }); return (Object.entries(functions) as [Keys, TState[Keys]][]).reduce( (acc, [key, v]) => ({ ...acc, [key]: () => store(v) }), {}, ) as Interface<TState, Keys>; } export type TState<States extends MapDef> = UnionState<States> & State; export function createState<TState extends State>(builder: StateCreator<TState>) { const state = builder(empty, empty, { getState: empty, setState: empty, destroy: empty, subscribe: empty, }); return { builder, keys: Object.keys(state) } as StateDef<TState, Keys<NonOptional<TState>>>; } // TODO: Global state as argument export function mergeStates<States extends MapDef>(states: States) { type StateKey = keyof States; const state: StateCreator<any> = (...args: Parameters<StateCreator<any>>) => Object.entries(states).reduce( (acc, [key, { builder, keys }]) => ({ ...acc, ...prefix(key, keys, builder, ...args), }), {}, ); let store: UseBoundStore<TState<States>>; if (process.env.NODE_ENV === "development") store = create(devtools(state)); else store = create(state); return { ...(Object.entries<StateDef<any>>(states).reduce( (acc, [key, { keys }]) => ({ ...acc, [key]: createInterface(store, keys as NonEmptyArray<StrKey<TState<States>>>, key), }), {}, ) as { [Key in StateKey]: Interface<ReturnState<States[Key]>, Str<States[Key]["keys"][number]>>; }), store, }; } export const addOptional = < T extends State, K1 extends NonEmptyArray<StrKey<NonOptional<T>>>, K2 extends NonEmptyArray<StrKey<Optional<T>>>, >( { builder, keys }: StateDef<T, K1>, o: K2, ) => ({ builder, keys: keys.concat(o as never) as NonEmptyArray<K1[number] | K2[number]> });
utils.types.ts
export type NonEmptyArray<T> = [T, ...T[]]; type RequiredKeys<T> = { [K in keyof T]: object extends { [P in K]: T[K] } ? never : K; }[keyof T]; export type Optional<T> = Omit<T, RequiredKeys<T>>; export type NonOptional<T> = Pick<T, NonNullable<RequiredKeys<T>>>; export type AnyFn = (...a: any[]) => any; export type Str<T> = NonNullable<Extract<T, string>>; export type StrKey<T> = Str<keyof T>;
auth/state.ts
import { addOptional, createState } from "utils/state"; type State = { token?: string; isLogged: boolean; logIn: (token: NonNullable<State["token"]>) => void; logOut: () => void; }; const def = createState<State>((set) => ({ isLogged: false, logIn(token) { set({ token, isLogged: true }); }, logOut() { set({ token: undefined, isLogged: false }); }, })); export const auth = addOptional(def, ["token"]);
state.ts
import { mergeStates } from "utils/state"; import { auth } from "auth/state"; // https://github.com/pmndrs/zustand/wiki // TODO: shared global state as argument export const useStore = mergeStates({ auth });
In component
const logged = useStore.auth.logged()
Something similar to this would facilitate store management.
utils/tools files dependency not found in a given example
May i know what version of zustand di you use? I do encountered some error in the code maybe the version I use. Currenty I am using the latest version of it.
How do I arrange the slices in a nice way and not merging them together into one? I was hoping to have multiple separate kinds of states based on the component I'm referring into.
// not this
export const useStore = create<MyState>((set, get) => ({
...createBearSlice(set, get),
...createFishSlice(set, get)
}));
// I wanted this, but I don't know how to update the state
export const useStore = create<MyState>((set, get) => ({
BearSlice: {...createBearSlice(set, get)},
FishSlice: {...createFishSlice(set, get)}
}));
If you don't need them to be merged, why not keep it simple?
const bearSlice = create<...>((set,get)=>({...}));
const fishSlice = create<...>((set,get)=>({...}));
const useStore = ()=>({
bears: bearSlice,
fish: fishSlice,
});
Any advice how to create a dynamic store? Maybe it's already here?
A game example:
export const useStore = create
export const useStore = create
Simple interface builder for easy composite between apps. Edited: update v4
/* eslint-disable @typescript-eslint/no-explicit-any */
import create, { StateCreator, StoreApi, UseBoundStore } from 'zustand'
import { devtools } from 'zustand/middleware'
import { UnionToIntersection } from 'type-fest'
type GetUse<T extends StateCreator<any>> = Omit<ReturnType<typeof merge<T[]>>, 'store'>
export const makeUse =
<States extends StateCreator<any>[]>(..._states: States) =>
<Callback extends (...args: any) => any>(callback: (use: GetUse<States[number]>) => Callback) =>
callback
export const merge = <T extends StateCreator<any>[]>(...arg: T) => {
type N<S> = S extends StateCreator<infer U> ? U : never
type States = Extract<UnionToIntersection<N<T[number]>>, object>
const stateCreator = ((...a) =>
arg.reduce(
(acc, cur) => ({
...acc,
...cur(...a),
}),
{}
)) as StateCreator<States>
let store: UseBoundStore<StoreApi<States>>
if (process.env['NODE_ENV'] === 'development') store = create(devtools(stateCreator as never))
else store = create(stateCreator)
return {
...Object.keys(store.getState()).reduce(
(acc, key) => ({ ...acc, [key]: () => store(state => state[key as keyof States]) }),
{} as { readonly [K in keyof States]: () => States[K] }
),
store,
} as const
}
}
@app/state
export const use = merge(stateCreator1, stateCreator2, ...)
Composable store hook
export const makeUseState1and2 = makeUse(stateCreator1, stateCreator2)(
use =>
() => {
const state1Field = use.state1Field();
const state2Field = use.state2Field();
return useMemo(() => state1Field + state2Field, [state1Field, state2Field]);
})
Use it in any app
import { use } from '@app/state';
export const useState1 = makeUseState1and2(use);
It only has one limitation, you have to work with null instead of undefined. There can't be optional or undefined fields in the initial state, since the interface is built from the initial state.
use
Edit: Got it! Thanks!
Thanks that Type was Dope and bit over my head! But I get a nicely merged 'store' inside 'use', alongside correctly merged states & actions in the objects root. However, there are no use.state1Fields so the hook cannot return, just the function itself. Can I bypass and go for use.store.getState() ?
use
Edit: Got it! Thanks!
Thanks that Type was Dope and bit over my head! But I get a nicely merged 'store' inside 'use', alongside correctly merged states & actions in the objects root. However, there are no use.state1Fields so the hook cannot return, just the function itself. Can I bypass and go for use.store.getState() ?
No. If you want to do this do in application for soundness. I.e.
type A = { a: string }
type B = A & { b: string }
type Check<T> = T extends UseBoundStore<StoreApi<B>> ? 'ok' : 'err'
type Checked = Check<UseBoundStore<StoreApi<A>>>
// Checked === 'err' test it
😏
store.ts
import create, { SetState, GetState } from 'zustand' import { FirstSlice, createFirstSlice } from 'first-slice.ts' import { SecondSlice, createSecondSlice } from 'second-slice.ts' export type StoreState = FirstSlice & SecondSlice export type StoreSlice<T> = ( set: SetState<StoreState>, get: GetState<StoreState> ) => T const useStore = create<StoreState>((set, get) => ({ ...createFirstSlice(set, get), ...createSecondSlice(set, get), }))
first-slice.ts
export type FirstSlice = {} export const createFirstSlice: StoreSlice<FirstSlice> = (set, get) => ({})
second-slice.ts
export type SecondSlice = {} export const createSecondSlice: StoreSlice<SecondSlice> = (set, get) => ({})
Now how to use middleware with this setup? Zustand docs are really confusing to me. specially I think the docs are not updated to latest version (v4)
@apudiu Slices in latest version (v4) with middleware
store.ts
export type StoreState = ProductSlice & CartSlice
export const useAppStore = create<StoreState>()(
persist(
devtools((...a) => {
return {
...createProductSlice(...a),
};
}, {name:'catugy-persist'}), { name: 'catugy-storage'}
)
);
createProductSlice.ts
import { StateCreator } from "zustand";
import { StoreState } from "../store";
export interface Product {
price: number;
title: string;
quantity?: number;
}
export interface ProductSlice {
products: Product[];
fetchProducts: () => void;
}
//Middlewares and their mutators reference
export const createProductSlice: StateCreator<StoreState, [["zustand/persist", unknown], ["zustand/devtools", never]], [], ProductSlice
> = (set) => ({
products: [],
fetchProducts: async () => {
const res = await fetch('https://api....')
set({ products: await res.json() })
},
})
index.tsx
const { products, fetchProducts } = useAppStore()
@apudiu Slices in latest version (v4) with middleware
store.ts
export type StoreState = ProductSlice & CartSlice export const useAppStore = create<StoreState>()( persist( devtools((...a) => { return { ...createProductSlice(...a), }; }, {name:'catugy-persist'}), { name: 'catugy-storage'} ) );
createProductSlice.ts
import { StateCreator } from "zustand"; import { StoreState } from "../store"; export interface Product { price: number; title: string; quantity?: number; } export interface ProductSlice { products: Product[]; fetchProducts: () => void; } //Middlewares and their mutators reference export const createProductSlice: StateCreator<StoreState, [["zustand/persist", unknown], ["zustand/devtools", never]], [], ProductSlice > = (set) => ({ products: [], fetchProducts: async () => { const res = await fetch('https://api....') set({ products: await res.json() }) }, })
index.tsx
const { products, fetchProducts } = useAppStore()
How can we apply this, to only persist one slice ? (or use separate middleware for each slice)
I've now maybe spent 3-4 hours trying to figure out the typescript for this. I would be done now if I made two separate stores. But from my understanding, the recommended way is to have only one store.
I update it and make a mini library. Give me a week. I'm pretty busy.
On Fri, May 20, 2022 at 11:29 AM icjhomz @.***> wrote:
May i know what version of zustand di you use? I do encountered some error in the code maybe the version I use. Currenty I am using the latest version of it.
— Reply to this email directly, view it on GitHub https://github.com/pmndrs/zustand/issues/508#issuecomment-1132684704, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAUBACPTRIUD7APPW6GN4WTVK5LO5ANCNFSM5A3PTVAA . You are receiving this because you commented.Message ID: @.***>
This won't work if there is a state that replies on each other. And please use TypeScript.
I've been to this thread a few times in the past, and found myself staring it in the face again today. I feel compelled to spot the elephant in the room here and suggest that all of the above circus act is good example of when the tools create more trouble than they're solving. That said, a couple of points to note:
- If your slices are co-dependent, you might want to re-evaluate whether they should be separate slices
- If your slices are not co-dependent, it might be easier for everyone (including devs who have to maintain your code) to leave them as separate stores.
- If your primary reason for splitting the slices was to compartmentalize lengthy code, you might be trying to do too much inside the slice.
I'm guilty of using slices to divide my code and using immer + devtools too. This will add to the complexity we were trying to avoid with redux.
Thanks, @ripvannwinkler for pointing it out.
exactly, there's no example code for someone using the slices pattern with persist middleware, and partializing fields for persisting
@Just-Moh-it here's a pull request with that exact pattern in it if you're curious. https://github.com/Shopify/blockchain-components/pull/196
Or if you just want the code (but in a very generic manner), here you go:
useStore.ts
import {create} from 'zustand';
import {immer} from 'zustand/middleware/immer';
import {createBarState} from './wallet/barState';
import {createFooState} from './modal/fooState';
import type {CombinedState, Set} from './types';
export const useStore = create<CombinedState>()(
persist(
immer((...api) => ({
bar: createBarState(...api),
foo: createFooState(...api),
})),
{
name: 'my-store-name',
partialize: (state) => ({
// Include the keys you want to persist in here.
bar: {
baz: state.bar.baz,
},
}),
merge: (persistedState, currentState) => {
// persistedState is unknown, so we need to cast it to CombinedState | undefined
const typedPersistedState = persistedState as CombinedState | undefined;
return {
bar: {
// We need to do a deep merge here because the default merge strategy is a
// shallow merge. Without doing this, our actions would not be included in
// our merged state, resulting in unexpected behavior.
...currentState.bar,
...(typedPersistedState?.bar || {}),
},
foo: currentState.foo,
};
},
},
),
);
barState.ts
import type {BarStateDefinition, BarStateType, StateSlice} from '../types';
const initialBarState: BarStateDefinition = {
baz: '',
qux: '',
};
export const createBarState: StateSlice<BarStateType> = (set) => ({
...initialBarState,
setBaz: (value) =>
set((state) => {
state.bar.baz = value;
}),
setQux: (value) =>
set((state) => {
state.bar.qux = value;
}),
});
fooState.ts
import type {FooStateDefinition, FooStateType, StateSlice} from '../types';
const initialFooState: FooStateDefinition = {
thud: '',
waldo: '',
};
export const createFooState: StateSlice<FooStateType> = (set) => ({
...initialFooState,
setThud: (value) =>
set((state) => {
state.foo.thud = value;
}),
setWaldo: (value) =>
set((state) => {
state.foo.waldo = value;
}),
});
types.ts
import {StateCreator} from 'zustand';
export interface BarStateDefintion {
baz: string;
qux: string;
}
export interface BarStateActions {
setBaz: (value: string) => void;
setQux: (value: string) => void;
}
export interface FooStateDefintion {
thud: string;
waldo: string;
}
export interface FooStateActions {
setThud: (value: string) => void;
setWaldo: (value: string) => void;
}
export type BarStateType = BarStateActions & BarStateDefintion;
export type FooStateType = FooStateActions & FooStateDefintion;
export interface CombinedState {
bar: BarStateType;
foo: FooStateType;
}
export type StateSlice<T> = StateCreator<
CombinedState,
[['zustand/immer', never]],
[['zustand/persist', Partial<T>]],
T
>;
Why isn't @QuintonC 's code in document? Very helpful, thank you!
Auto generating selectors stops at
bar
andfoo
property. It's doesn't' generate selectors forthud
,waldo
,setThud
orsetWaldo
. Is this the intended behavior? If not is there a solution for this?
Auto generating selectors stops at
bar
andfoo
property. It's doesn't' generate selectors forthud
,waldo
,setThud
orsetWaldo
. Is this the intended behavior? If not is there a solution for this?
@Armadillidiid Are you referring to the example I provided? If so, can you provide a CodeSandbox so I can take a look?
exactly, there's no example code for someone using the slices pattern with persist middleware, and partializing fields for persisting
@Just-Moh-it here's a pull request with that exact pattern in it if you're curious. Shopify/blockchain-components#196
Or if you just want the code (but in a very generic manner), here you go:
useStore.ts
import {create} from 'zustand'; import {immer} from 'zustand/middleware/immer'; import {createBarState} from './wallet/barState'; import {createFooState} from './modal/fooState'; import type {CombinedState, Set} from './types'; export const useStore = create<CombinedState>()( persist( immer((...api) => ({ bar: createBarState(...api), foo: createFooState(...api), })), { name: 'my-store-name', partialize: (state) => ({ // Include the keys you want to persist in here. bar: { baz: state.bar.baz, }, }), merge: (persistedState, currentState) => { // persistedState is unknown, so we need to cast it to CombinedState | undefined const typedPersistedState = persistedState as CombinedState | undefined; return { bar: { // We need to do a deep merge here because the default merge strategy is a // shallow merge. Without doing this, our actions would not be included in // our merged state, resulting in unexpected behavior. ...currentState.bar, ...(typedPersistedState?.bar || {}), }, foo: currentState.foo, }; }, }, ), );
barState.ts
import type {BarStateDefinition, BarStateType, StateSlice} from '../types'; const initialBarState: BarStateDefinition = { baz: '', qux: '', }; export const createBarState: StateSlice<BarStateType> = (set) => ({ ...initialBarState, setBaz: (value) => set((state) => { state.bar.baz = value; }), setQux: (value) => set((state) => { state.bar.qux = value; }), });
fooState.ts
import type {FooStateDefinition, FooStateType, StateSlice} from '../types'; const initialFooState: FooStateDefinition = { thud: '', waldo: '', }; export const createFooState: StateSlice<FooStateType> = (set) => ({ ...initialFooState, setThud: (value) => set((state) => { state.foo.thud = value; }), setWaldo: (value) => set((state) => { state.foo.waldo = value; }), });
types.ts
import {StateCreator} from 'zustand'; export interface BarStateDefintion { baz: string; qux: string; } export interface BarStateActions { setBaz: (value: string) => void; setQux: (value: string) => void; } export interface FooStateDefintion { thud: string; waldo: string; } export interface FooStateActions { setThud: (value: string) => void; setWaldo: (value: string) => void; } export type BarStateType = BarStateActions & BarStateDefintion; export type FooStateType = FooStateActions & FooStateDefintion; export interface CombinedState { bar: BarStateType; foo: FooStateType; } export type StateSlice<T> = StateCreator< CombinedState, [['zustand/immer', never]], [['zustand/persist', Partial<T>]], T >;
This should be in the docs, worked for me, thanks @QuintonC !
Auto generating selectors stops at
bar
andfoo
property. It's doesn't' generate selectors forthud
,waldo
,setThud
orsetWaldo
. Is this the intended behavior? If not is there a solution for this?@Armadillidiid Are you referring to the example I provided? If so, can you provide a CodeSandbox so I can take a look?
@QuintonC I'll be sure to provide the CodeSandbox link shortly.
However, I do have another question that I hope you might shed some light on. I'm trying to create duplicate slices within the same store, but I'm encountering an unexpected behavior where the foo
slice and the fooDuplicate
slice seem to share the same state, despite being under different objects.
I've included the code snippet below for reference:
const useGenericStoreBase = create<CombinedState>()(
devtools(
persist(
immer(withLenses((...args) => ({
foo: createHiddenSlice(...args),
bar: createGenericSlice(...args),
fooDuplicate: createHiddenSlice(...args)),
}))),
{
name: "viewedGenericData",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ ...state.bar.viewedGenericData }),
}
),
{
name: "Generic Store",
}
)
);
There is now official documentation about using the slices pattern with TypeScript
Hi
Sorry if this has been asked already. I looked at the Wiki on how to split up my store into separate slices, but how can I go about this using Typescript?