I would like to implement an undo/redo fonctionality to each elements inside a dictionary and I thought that I could probably do it with Immer patches feature.
Unfortunatly I did not find any way to recover them after an action with createSlice or createReducer, and the word patches is not mentioned in the documentation of RTK.
Since patches are available after the state is changed I can see why it could be something pretty hard to expose to the user with a clear guidance on how it could be used (I'm still unsure to be honest).
Maybe I'm on the wrong track or I shouldn't be using RTK for this specific need, in any case thanks for the package and thanks in advance for any help you can provide.
We don't expose those APIs, because we don't use them at all in RTK.
Given that Immer is a transitive dependency, you should be able to explicitly import those from Immer yourself: import {patches} from 'immer'.
This will _likely_ work as-is due to how NPM and Yarn install packages these days, but to be safe you'd want to add an explicit dependency on Immer in your own app.
Note that we currently depend on immer@^4, not ^5, so you'd want to specify that so you only have one version of Immer in your app. We hope to update to the latest version of Immer once the next major version comes out with improvements to bundle sizes and tree shaking.
Thank you for your reply but what I was talking about is either recovered after a produceWithPatches or as the third argument of produce (or createNextState since you've named it like that in RTK) and I think I shouldn't use produce inside RTK reducers, again drafting the draft, just to get the patches.
So I guess I'll opt-out of RTK for this very specific reducers use case.
Hi yes @markerikson I think either you're talking about something else or I'm confused.
I'm looking to use these for optimistic UI changes following this comment here: https://github.com/reduxjs/redux-toolkit/issues/500#issuecomment-619511457
Are the patches that @fdel-car and I are referring to now exposed or could you elaborate on your comment. Thanks
Uh... _I'm_ confused about what _you're_ confused about :)
We still do not re-export those methods ourselves, because we still do not use them in RTK. Per my previous comment, you should be able to add a dependency on Immer in your own app (which will not change the actual _installed_ list of deps since RTK depends on Immer itself).
So, first yarn add immer so that it's listed in your own package.json, and then import {patchesOrWhatever} from "immer" in your own app.
Apologies for that 馃槃
Let me try to explain.
RTK uses immer to do the state mutations in the caseReducers as part of createSlice etc.
I was hoping/expecting that we'd be able to get the patch that occurred with that caseReducer. I think this would be useful for several things (like optimistic UI).
Of course I can import immer and whatever I want from it.... that's not what I'm after. I just know that behind the scenes RTK is using it and I want to be able to access some of results from immer as it's used by RTK.
Hmm. Adding patches & reversePatches will increase the bundle size by about 1kb for all our users (immer tree-shakes and currently is skipping the patches part when shipped with RTK).
In addition to that: even if we enabled those, how would you access these?
The only place where RTK uses produce is in createReducer (and as a consequence in createSlice. Now, a reducer always returns the new state. So it won't be part of the return value.
We could also add it to the signature of a reducer, like (state, action, patchFn?) => state, but that would not really help as well. Reducers are called by redux, so you won't be able to pass that third argument in.
Additionally, not every code path within a createReducer call actually calls produce - if you pass in a non-immerable value, the reducer will be called normally. And for handling those cases, we would have to add additional logic (= again, more bundle size - also more complexity to maintain).
So I don't really see how this would be used in reality, it would add extra code complexity and quite significant bundle size for all consumers of RTK. I'm sorry, but this doesn't seem like a reasonable addition.
I'm about to implement undo / redo on my app that's already using RTK.
Immer makes this so easy, so it's a real shame we can't take advantage of this when using createSlice or createReducer.
Rather than adding extra complexity and bloat to these methods, could we create something new e.g. createReducerWithPatches?
I can appreciate the complexities from looking over the source code. I guess we wouldn't necessarily need to pass in a callback. What if they were added to a specified part of the store?
We're definitely not going to add new APIs that would be that specific to the patches use case (or any other use case, really).
If someone can propose a good option for making this somehow configurable, I'm willing to at least discuss the idea, but thus far it doesn't seem like that's really feasible.
Yeah makes sense. I feel if we were using plain old Redux this would be pretty easy to implement.
The only alternative I can think of would be to have an option to disable immer, that way we could then use immer how we wish (or use something else). Currently we would need to convert the draftState to it's original form, which is straight forward enough to do, but seems like it could cause perf issues
setCurrentFileId: (draftState, action: PayloadAction<string>) => {
const fileId = action.payload;
const [nextState, patches, inversePatches] = produceWithPatches(
original(draftState), // <----
(draft) => {
draft.data.currentFileId = fileId;
}
);
return nextState;
},
Still... if you wanted to do anything with those patches, it would have to be a side effect, with the risk of introducing tons of bugs as soon as you use the devtools, right?
This is about as clean as solution as I've got so far
setCurrentFileId: (draftState, action: PayloadAction<string>) => {
const fileId = action.payload;
const [nextState, patches, inversePatches] = produceWithPatches(
original(draftState),
(draft) => {
draft.data.code.currentFileId = fileId;
}
);
return produce(nextState, (draft) => {
draft.undoStack.push({ patches, inversePatches });
});
},
Ah, makes sense. Btw., accessing original like that won't probably have any runtime implications.
Btw., accessing
originallike that won't probably have any runtime implications.
Yes I think you are right actually. Do you mean when NODE_ENV === production?
So this is what I've ended up with, and I'm pretty happy with it tbh:
const produceWithPatch = (
state,
undoable,
mutations
) => {
if (!undoable) {
mutations(state);
return;
}
const [nextState, patches, inversePatches] = produceWithPatches(
original(state),
(draft) => {
mutations(draft);
}
);
return produce(nextState, (draft) => {
draft.undoStackPointer++;
draft.undoStack.length = draft.undoStackPointer;
draft.undoStack[draft.undoStackPointer] = {
patches,
inversePatches
};
});
};
and then used inside createSlice(...
setCurrentFileId: (state, action) => {
const { fileId, undoable } = action.payload;
return produceWithPatch(state, undoable, (draft) => {
draft.currentFileId = fileId;
});
}
Btw., accessing
originallike that won't probably have any runtime implications.Yes I think you are right actually. Do you mean when
NODE_ENV === production?
Pretty much always, as it is just a very simple function:
/** Get the underlying object that is represented by the given draft */
/*#__PURE__*/
export function original<T>(value: T): T | undefined
export function original(value: Drafted<any>): any {
if (!isDraft(value)) die(23, value)
return value[DRAFT_STATE].base_
}
So this is what I've ended up with, and I'm pretty happy with it tbh:
[...]
Looking nice :+1:
Most helpful comment
Thank you for your reply but what I was talking about is either recovered after a
produceWithPatchesor as the third argument ofproduce(orcreateNextStatesince you've named it like that in RTK) and I think I shouldn't useproduceinside RTK reducers, again drafting the draft, just to get the patches.So I guess I'll opt-out of RTK for this very specific reducers use case.