pmndrs / zustand

🐻 Bear necessities for state management in React
https://zustand-demo.pmnd.rs/
MIT License
48.27k stars 1.5k forks source link

[Question] Creating slices using Typescript #508

Closed KeeganFargher closed 3 years ago

KeeganFargher commented 3 years ago

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?

AnatoleLucet commented 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)
}));
KeeganFargher commented 3 years ago

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!

AnatoleLucet commented 3 years ago

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

KeeganFargher commented 3 years ago

Awesome, thanks so much for the help!

darkbasic commented 3 years ago

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.

focux commented 3 years ago

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>;
cabanossi commented 3 years ago

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' are callable. Type 'StoreApi' has no call signatures.ts(2349)

without the | StoreApi<T>; is all fine!

darkbasic commented 3 years ago

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.

cabanossi commented 3 years ago

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! ;-)

unsady commented 3 years ago

😏

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) => ({})
zzau13 commented 2 years ago

updated https://github.com/pmndrs/zustand/issues/508#issuecomment-1200443303

lveillard commented 2 years ago

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

cleferman commented 2 years ago

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.

igdev116 commented 2 years ago

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));
regohiro commented 2 years ago

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.

ripvannwinkler commented 2 years ago

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:

icjhomz commented 2 years ago

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

zzau13 commented 2 years ago

updated https://github.com/pmndrs/zustand/issues/508#issuecomment-1200443303

icjhomz commented 2 years ago

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.

jmmaa commented 2 years ago

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)}
}));
ripvannwinkler commented 2 years ago

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,
    });
codcodea commented 2 years ago

Any advice how to create a dynamic store? Maybe it's already here?

A game example:

export const useStore = create((set, get) => ({ ...basicCharacter(set, get), ...worldA(set, get) ...levelOneActions(set, get) }));

export const useStore = create((set, get) => ({ ...basicCharacter(set, get), ...weekendSuperSkills(set, get) ...worldA(set, get) ...levelTwoActions(set, get) }));

zzau13 commented 2 years ago

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.

codcodea commented 2 years ago
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() ?

zzau13 commented 2 years ago
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
apudiu commented 2 years ago

😏

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)

ecatugy commented 2 years ago

@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()
orangeswim commented 2 years ago

@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.

zzau13 commented 2 years ago

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: @.***>

elhe26 commented 2 years ago

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.

QuintonC commented 1 year ago

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
>;
Toshinaki commented 1 year ago

Why isn't @QuintonC 's code in document? Very helpful, thank you!

Armadillidiid commented 1 year ago

Auto generating selectors stops at bar and foo property. It's doesn't' generate selectors for thud, waldo, setThud or setWaldo. Is this the intended behavior? If not is there a solution for this?

QuintonC commented 1 year ago

Auto generating selectors stops at bar and foo property. It's doesn't' generate selectors for thud, waldo, setThud or setWaldo. 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?

soulbliss commented 1 year ago

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 !

Armadillidiid commented 1 year ago

Auto generating selectors stops at bar and foo property. It's doesn't' generate selectors for thud, waldo, setThud or setWaldo. 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",
    }
  )
);
willpinha commented 4 months ago

There is now official documentation about using the slices pattern with TypeScript