Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Immer bans assignment to the nested parts of state #4830

Open
MostFrumiousBandersnatch opened this issue Jan 26, 2025 · 4 comments
Open

Immer bans assignment to the nested parts of state #4830

MostFrumiousBandersnatch opened this issue Jan 26, 2025 · 4 comments

Comments

@MostFrumiousBandersnatch

Hello!

Recently faced with some strange behavior of reducers, trying to create a generic slice creator:

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

type BasicEntity = object;

type Patch<V> ={
  value: V;
};
  
type MyOwnDraft<E extends BasicEntity> = Partial<{
  [K in keyof E]: Patch<E[K]>;
}>;

type WorkingCopy<E extends BasicEntity> = {
  original?: E;
  myOwnDraft: MyOwnDraft<E>;
};

const init = <E extends BasicEntity>(): WorkingCopy<E> => ({
  original: undefined,
  myOwnDraft: {},
});

const load = <E extends BasicEntity>(instance: E): WorkingCopy<E> => ({
  original: instance,
  myOwnDraft: {},
});

const discard = <E extends BasicEntity>(
  wc: WorkingCopy<E>
): WorkingCopy<E> => ({
  original: wc.original,
  myOwnDraft: {},
});

const createFormSlice = <E extends BasicEntity>(name: string) =>
  createSlice({
    name: `Form ${name}`,
    initialState: { workingCopy: init<E>() },
    reducers: {
      load: (_state, action: PayloadAction<E>) => ({
        workingCopy: load(action.payload),
      }),
      discard: (state) => {
        state.workingCopy.myOwnDraft = {};
      },
     //edit and the rest of the stuff
    },
  });

type SomeEnt = {
  id: number;
  name: string;
};

const someSlice = createFormSlice<SomeEnt>('some');
const initial: SomeEnt = { id: 1, name: 'some' };
someSlice.actions.load(initial);
discard(load(initial));

(stack-blitz)
in discard reducer i got:

Type '{}' is not assignable to type 'Draft<Partial<{ [K in keyof E]: Patch<E[K]>; }>>'.(2322)

Image

That's fixable with workaround like

state.workingCopy.myOwnDraft = castDraft<MyOwnDraft<E>>({});

But i supposed immer to be an invisible companion of redux-toolkit, and it looks weird anyway.

Similar code outside (written in immutable manner) of reducer causes not problems with type-checker.

Could you please explain what I've done wrong?
Thank you in advance!

@markerikson
Copy link
Collaborator

markerikson commented Jan 26, 2025

I don't think this is an Immer issue per se, so much as it's a use of the generics and wrapper function around createSlice, and then use of that generic inside of the state type.

You can wrap createSlice, but it requires some specific patterns, which we have documented here:

@EskiMojo14
Copy link
Collaborator

Image
this is easy to reproduce without createSlice at all - it's just that Typescript doesn't know for certain what Draft<Partial<{ [K in keyof E]: Patch<E[K]>; }>> would resolve to (Draft is a long conditional type accounting for various different types) because it doesn't know what E is. Therefore, it doesn't feel confident that {} would satisfy it, even if you are.

ultimately since this is solveable with castDraft and not related to RTK at all, i think this is closeable

@MostFrumiousBandersnatch
Copy link
Author

MostFrumiousBandersnatch commented Jan 27, 2025

Therefore, it doesn't feel confident that {} would satisfy it, even if you are.

Well, i understood the reason of that from the typescript's point of view. The question is how to avoid that, using RTK. This typing issue brought by Immer, and i can't even write this reducer in immutable manner.

You can wrap createSlice, but it requires some specific patterns, which we have documented here:

Oh, that requirements do not make a lot of sense, either. I tried to specify state type explicitly in the reducer before raising this issue. Using (state: WritableDraft<FormState<E>>) does not help as well.

Image

ultimately since this is solveable with castDraft and not related to RTK at all, i think this is closeable

I mention this workaround to only demonstrate how ugly and invasive Immer is, considering reducers, but of course,
That's a point i can hardly argue with. Nevertheless, I'd dare to express my personal vision on regarding this problem:

The super-power of RTK, which i personally love it for, is how it elegantly binds actions and reducers together, allowing create generic things. Immer, in the context of RTK, is a grass-cutter hard-plugged to a sport car. You still can use it for trivial purposes, but you'd better not try to speed it up.
It's awkwardly typed, it degrades performance of reducers, and i still wonder which things it good at (in the context of RTK)?
I know your position regarding Immer, but i humbly ask to reconsider it. I believe that users can consciously whether they need immer, or they prefer another tools to modify the state (e.g. https://akheron.github.io/optics-ts/).

Sorry, i was too emotional and thank you for this brilliant library.

@markerikson
Copy link
Collaborator

markerikson commented Jan 27, 2025

@MostFrumiousBandersnatch our stance on Immer hasn't changed. It's a core part of RTK, and it's the right choice for our users. I gave an extensive re-answer on this recently:

Per that comment, we've experimented with trying to make use of Immer configurable, and it's too complex and not on our roadmap atm.

It's awkwardly typed, it degrades performance of reducers, and i still wonder which things it good at (in the context of RTK)

The types work fine out of the box. There _are _edge cases, and you're running into one of them, specific to dealing with wrapping and use of generics.

Per that issue, yes Immer has a perf impact, but most of the time it's not a problem.

Immer still simplifies reducers and prevents accidental mutations.

Overall, Immer does work great in RTK, and we're not changing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants