Skip to content

Does not play well with reducers created with redux-toolkit's createSlice #272

@posita

Description

@posita

This might be a duplicate of #268, but I think this weighs in favor of treating #268 as a real bug (instead of as-designed) and actually fixing it. The short version is as follows. I created a slice with createSlice from reduxjs/redux-toolkit with an incrementNextId action:

export interface SliceState {
  next_id: number;
}

const INITIAL_STATE: SliceState = {
  next_id: 1,
}

export const slice = createSlice({
  name: "slice",
  initialState: INITIAL_STATE,
  reducers: {
    incrementNextId: { /* ... */ }
  },
})

export const {
  incrementNextId,
} = slice.actions

export const slice_reducer = slice.reducer
export const undoable_slice_reducer = undoable(slice_reducer)

Here's the rub. Using the plain reducer works just like it should according to Redux's instructions on how to test reducers:

  describe("plain increment", () => {
    // this passes
    it("should increment next_id", () => {
      var state = slice.slice_reducer(undefined, slice.incrementNextId({}))
      expect(state.next_id).toEqual(2)
    })
  })

Using the wrapped reducer does not with a nearly identical test (the only difference is which reducer is used and how we inspect state):

  describe("undoable increment", () => {
    // this fails
    it("should increment next_id", () => {
      var state = slice.undoable_slice_reducer(undefined, slice.incrementNextId({}))
      expect(state.present.next_id).toEqual(2)  // nope, the value is actually 1
    })
  })

If I modify the second test as follows, it passes, which doesn't seem right:

  describe("undoable increment", () => {
    // this works, but it shouldn't
    it("should increment next_id", () => {
      var state = slice.undoable_slice_reducer(undefined, slice.incrementNextId({}))  // ignored?
      state = slice.undoable_slice_reducer(state, slice.incrementNextId({}))  // this time, with feeling!
      expect(state.present.next_id).toEqual(2)  // works now?
    })
  })

Here's where things get totally off the rails. This fails both tests:

  describe("undoable increment", () => {
    // this fails
    it("should increment next_id", () => {
      var state = slice.undoable_slice_reducer(undefined, slice.incrementNextId({}))
      expect(state.present.next_id).toEqual(2)  // nope, the value is actually 1
    })

    // this fails, too
    it("should also increment next_id", () => {
      var state = slice.undoable_slice_reducer(undefined, slice.incrementNextId({}))
      expect(state.present.next_id).toEqual(2)  // nope, the value is actually 1
    })
  })

If I make the above modification to the first test, both tests now pass:

  describe("undoable increment", () => {
    // this works, but it shouldn't
    it("should increment next_id", () => {
      var state = slice.undoable_slice_reducer(undefined, slice.incrementNextId({}))  // ignored?
      state = slice.undoable_slice_reducer(state, slice.incrementNextId({}))  // this time, with feeling!
      expect(state.present.next_id).toEqual(2)  // works now?
    })

    // this now…works?
    it("should also increment next_id", () => {
      var state = slice.undoable_slice_reducer(undefined, slice.incrementNextId({}))
      expect(state.present.next_id).toEqual(2)  // now this passes; WTF?!
    })
  })

If I move that approach to the second test, only it passes:

  describe("undoable increment", () => {
    // this fails
    it("should increment next_id", () => {
      var state = slice.undoable_slice_reducer(undefined, slice.incrementNextId({}))
      expect(state.present.next_id).toEqual(2)  // nope, the value is actually 1
    })

    // this now…works?
    it("should also increment next_id", () => {
      var state = slice.undoable_slice_reducer(undefined, slice.incrementNextId({}))  // ignored?
      state = slice.undoable_slice_reducer(state, slice.incrementNextId({}))  // this time, with feeling!
      expect(state.present.next_id).toEqual(2)  // works now?
    })
  })

Complete source code is available via posita/redux-toolkit-undo-clash.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions