pmndrs / zustand

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

Add example for TypeScript inferred types with Immer #191

Closed gauravkumar37 closed 3 years ago

gauravkumar37 commented 4 years ago

I would like to add a TypeScript example which combines combine and immer usages so that everything is typed, automatically inferred and is intended for use with immer. Typing is inferred in state actions as well as when using it in the components.

  1. Here, withImmer should be the innermost middleware and other middlewares can be added on top of it such as devools.
  2. The 2nd parameter of set function (replace) is omitted here, since it doesn't make sense with Immer because the entire object (original/proxied) is returned.
  3. It should be noted that using shallow in useStore(..., shallow) is also not needed because if the object is not modified, Immer returns the original object and strict equality === will hold.
  4. In the current version of immer example, explicit type casting is used because currying is used for the producer syntax. I have explicitly used the original form of producer with 2 arguments so that no casting is required.
const withImmer = <PrimaryState extends State, SecondaryState extends State>(
  initialState: PrimaryState,
  createState: (
    set: (fn: (draftState: PrimaryState) => void) => void,
    get: GetState<PrimaryState>,
    api: StoreApi<PrimaryState>
  ) => SecondaryState
): StateCreator<PrimaryState & SecondaryState> =>
  (set, get, api) =>
    Object.assign(
      {},
      initialState,
      createState(
        fn => set(baseState => produce(baseState, fn)),
        get as GetState<PrimaryState>,
        api as StoreApi<PrimaryState>
      )
    );

Usage:

const initialState = {data: {count: 0, other: true}};
const useStore = create(withImmer(initialState, set => ({
    incBy: (by: number) => set(state => {
      state.data.count += by;
    }),
    invertOther: () => set(state => {
      state.data.other = !state.data.other;
    }),
  }))
);

const [count, incBy, invertOther] = useStore(state => [state.data.count, state.incBy, state.invertOther]);
console.log(count); // 0
incBy(10);
console.log(count); // 10

Please let me know your thoughts on the same.

dai-shi commented 4 years ago

Nice!

If we add CustomSetState in combine, can we create withImmer with combine?

gauravkumar37 commented 4 years ago

If we add CustomSetState in combine, we would still have to actually call produce function in combine in https://github.com/react-spring/zustand/blob/7b2391b74ab19730d10f2709bb91528b838045f8/src/middleware.ts#L111 Is there a way in TS which can help us do that?

dai-shi commented 4 years ago

Yeah, I mean my question is if we can create withImmer having combine in it.

dai-shi commented 4 years ago

@gauravkumar37 I finally made it. It was not combine but StateCreator that we need to modify. Check this: https://codesandbox.io/s/beautiful-wilbur-ftvdt

type StateCreator<
  T extends State,
  CustomSetState = SetState<T>,
  U extends State = T
> = (set: CustomSetState, get: GetState<T>, api: StoreApi<T>) => U;

const immer = <T extends State, U extends State>(
  config: StateCreator<T, (fn: (draft: T) => void) => void, U>
): StateCreator<T, SetState<T>, U> => (set, get, api) =>
  config((fn) => set(produce(fn) as (state: T) => T), get, api);

const combineAndImmer = <
  PrimaryState extends State,
  SecondaryState extends State
>(
  initialState: PrimaryState,
  config: StateCreator<
    PrimaryState,
    (fn: (draft: PrimaryState) => void) => void,
    SecondaryState
  >
): StateCreator<PrimaryState & SecondaryState> => {
  return combine(initialState, immer(config));
};

I have explicitly used the original form of producer with 2 arguments so that no casting is required.

This doesn't seem to work in the codesandbox above. Not sure.

dai-shi commented 3 years ago

@gauravkumar37 Would you like to continue work on it? I think it's going to produce something good.

gauravkumar37 commented 3 years ago

@dai-shi Unfortunately I'm not working on React for the time being, so it would be difficult for me to take this forward.

dai-shi commented 3 years ago

@gauravkumar37 No problem!

Let's leave this issue open for a while and then close.

agcty commented 3 years ago

Has this been solved already?

dai-shi commented 3 years ago

I'm not sure. We changed types in newer versions, so there might not be any issues.

always-maap commented 3 years ago

Nah, the combine with immer still can't infer types(which is reasonable) https://stackblitz.com/edit/react-ts-hnrmpk?file=useCounter.tsx

dai-shi commented 3 years ago

216 is a related issue.

Basically, I suppose typing issue with combine and other middleware is not solved.

always-maap commented 3 years ago

anything I can help with? Testing, example, docs super interested in Poimandres group work

dai-shi commented 3 years ago

@always-maap sure! This typing issue would require decent TS knowledge. You can challenge it, if you like. For other tasks, we want to have more basic and advanced examples in https://github.com/pmndrs/zustand#best-practices (contents are in wiki pages), and https://github.com/pmndrs/zustand/issues have help wanted label, as well as Type: Question and documentation. They are all waiting for contributors.

just-be-weird commented 4 months ago

@dai-shi was this issue resolved, and how? I'm facing this same issue on latest version of zustand with immer with combine API

dai-shi commented 4 months ago

This is pretty old. https://github.com/pmndrs/zustand/issues/191#issuecomment-691585896 is the last status. If that doesn't help, please open a new discussion.