Type problem with wrapped createSlice #4020
-
I am working on a generic undo/redo feature, but having one typing problem remaining that I somehow can't figure out myself. Maybe someone else can have a look, but I am fully aware that this is quite a nontrivial question :-( Here is my generic slice that wraps import {
CaseReducer,
createSlice,
Draft,
original,
PayloadAction,
SliceCaseReducers,
ValidateSliceCaseReducers,
} from "@reduxjs/toolkit"
import { applyPatches, castDraft, Patch, produce, produceWithPatches } from "immer"
type PatchesState = { undo: Patch[]; redo: Patch[] }
export type HistoryState<T> = {
past: PatchesState[]
present: T
future: PatchesState[]
}
export const withHistory = <S, P>(reducerFn: CaseReducer<S, PayloadAction<P>>) => ({
reducer(
draftState: Draft<HistoryState<S>>,
action: PayloadAction<P, string, { undoable?: boolean }>
) {
const { undoable } = action.meta
if (!undoable) {
const result = reducerFn(draftState.present, action)
if (result !== undefined) draftState.present = result as Draft<S>
return undefined
}
const state = original(draftState) as HistoryState<S>
const [nextState, redoPatch, undoPatch] = castDraft(
produceWithPatches<HistoryState<S>, Draft<HistoryState<S>>>(state, (draft) => {
const result = reducerFn(draft.present, action)
if (result !== undefined) draft.present = result as Draft<S>
})
)
return produce(nextState, (draft) => {
draft.past.push({
undo: undoPatch,
redo: redoPatch,
})
draft.future = []
})
},
prepare(payload: P, options?: { undoable: boolean }) {
const undoable = options?.undoable ?? true
return { payload, meta: { undoable } }
},
})
export const createHistorySlice = <T, Reducers extends SliceCaseReducers<HistoryState<T>>>({
name,
initialState,
reducers,
}: {
name: string
initialState: T
reducers: ValidateSliceCaseReducers<HistoryState<T>, Reducers>
}) => {
const historyInitialState: HistoryState<T> = {
past: [],
present: initialState,
future: [],
}
return createSlice({
name,
initialState: historyInitialState,
reducers: {
undo(state) {
const historyEntry = state.past.pop()
if (historyEntry) {
applyPatches(state, historyEntry.undo)
state.future.unshift(historyEntry)
}
},
redo(state) {
const historyEntry = state.future.shift()
if (historyEntry) {
applyPatches(state, historyEntry.redo)
state.past.push(historyEntry)
}
},
...reducers,
},
})
} The problem is that when I use it like this: import { createHistorySlice, withHistory } from "./historySlice"
type DataState = { [key: string]: any }
export const dataSlice = createHistorySlice({
name: "data",
initialState: {},
reducers: {
resetData() {
return {
past: [],
present: {},
future: [],
}
},
changeValue: withHistory<DataState, { fieldId: string; value: any }>((state, action) => {
const { fieldId, value } = action.payload
state[fieldId] = value
}),
},
})
export const { resetData, changeValue } = dataSlice.actions
export default dataSlice.reducer I get an type error when using the action: changeValue({ fieldId: "foo", value: 1 })
Somehow the arguments of
The strange thing is that when I comment out the |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 10 replies
-
If anyone is interested or wants to implement some undo/redo functionality, here is how I finally handled it. I stopped fiddling around with the reducers and just wrote a import {
createSlice,
Draft,
original,
SliceCaseReducers,
ValidateSliceCaseReducers,
} from "@reduxjs/toolkit"
import { applyPatches, castDraft, isDraft, Patch, produce, produceWithPatches } from "immer"
type PatchesState = { undo: Patch[]; redo: Patch[] }
export type HistoryState<T> = {
past: PatchesState[]
present: T
future: PatchesState[]
}
export const produceWithHistory = <S>(
draftState: Draft<HistoryState<S>> | HistoryState<S>,
recipe: (draft: Draft<S>) => Draft<S> | void,
options?: { undoable: boolean }
) => {
const state = (isDraft(draftState) ? original(draftState) : draftState) as HistoryState<S>
const [nextState, redoPatch, undoPatch] = castDraft(
produceWithPatches<HistoryState<S>, Draft<HistoryState<S>>>(state, (draft) => {
const result = recipe(draft.present)
if (result !== undefined) draft.present = result
})
)
const undoable = options?.undoable ?? true
if (undoable) {
produce(nextState, (draft) => {
draft.past.push({
undo: undoPatch,
redo: redoPatch,
})
draft.future = []
})
}
}
export const createHistorySlice = <T, Reducers extends SliceCaseReducers<HistoryState<T>>>({
name,
initialState,
reducers,
}: {
name: string
initialState: HistoryState<T>
reducers: ValidateSliceCaseReducers<HistoryState<T>, Reducers>
}) =>
createSlice({
name,
initialState,
reducers: {
undo(state) {
const historyEntry = state.past.pop()
if (historyEntry) {
applyPatches(state, historyEntry.undo)
state.future.unshift(historyEntry)
}
},
redo(state) {
const historyEntry = state.future.shift()
if (historyEntry) {
applyPatches(state, historyEntry.redo)
state.past.push(historyEntry)
}
},
...reducers,
},
}) And how I use it ... import { PayloadAction } from "@reduxjs/toolkit"
import { HistoryState, createHistorySlice, produceWithHistory } from "./historySlice"
type DataState = { [key: string]: any }
const initialState: HistoryState<DataState> = {
past: [],
present: {},
future: [],
}
export const dataSlice = createHistorySlice({
name: "data",
initialState,
reducers: {
resetData() {
return {
past: [],
present: {},
future: [],
}
},
changeValue(state, action: PayloadAction<{ fieldId: string; value: any }>) {
const { fieldId, value } = action.payload
produceWithHistory(state, (draft) => {
draft[fieldId] = value
})
},
},
})
export const { undo, redo, resetData, changeValue } = dataSlice.actions
changeValue({ fieldId: "foo", value: 1 })
export default dataSlice.reducer |
Beta Was this translation helpful? Give feedback.
-
@EskiMojo14 Thanks for sharing. The adapter approach looks interesting and I think I'll give it a try. For my use case, it will fit quite well. But I am also looking forward to custom slice creators as those would IMHO better fit third-party extensions (using the adapter looks a bit verbose to me). |
Beta Was this translation helpful? Give feedback.
Thanks for raising the issue regarding the regression with wrapping
createSlice
! Admittedly I've never been particularly keen on the approach as the typing forcreateSlice
is pretty fragile (as demonstrated by this regression).As an alternative, have you considered an "adapter" approach like we've done for
createEntityAdapter
? See this codesandbox for a proper demo.We're also investigating "custom slice creators" which would have the capability of creating multiple reducers in one go.