Search code examples
typescriptreduxredux-toolkit

What is considered to be an anti-pattern when updating state using createSlice from Redux-Toolkit?


I have the following createSlice code where there is the reducer setCustomEquipment that modifies the state. Is the = approach a valid way to mutate the state when using createSlice, or should I use the shallow state copy approach anyway regardless of whether I am using createSlice or not?

export const someSlice = createSlice({
  name: 'some',
  initialState,
  reducers: {
    setCustomEquipment: (state, action: PayloadAction<CustomEquipment[]>) => {
      // Variant A 
      state.customEquipment = action.payload;

      // Variant B
      // return {
      // ...state, { state.customEquipment : action.payload }
      // }
    },

Solution

  • Either method you are using is fine, and entirely your decision.

    Mutable update

    setCustomEquipment: (state, action: PayloadAction<CustomEquipment[]>) => {
      state.customEquipment = action.payload;
    }
    

    Return shallow copy into new reference

    setCustomEquipment: (state, action: PayloadAction<CustomEquipment[]>) => {
      return {
        ...state,
        customEquipment: action.payload,
      };
    }
    

    When using Redux-Toolkit slice reducers the allowed methods of updating state are to write mutable state update or to return a new state reference, e.g. a shallow copy of the previous state with overwritten properties.

    See Direct State Mutation for details.

    To make things easier, createReducer uses immer to let you write reducers as if they were mutating the state directly. In reality, the reducer receives a proxy state that translates all mutations into equivalent copy operations.

    Writing "mutating" reducers simplifies the code. It's shorter, there's less indirection, and it eliminates common mistakes made while spreading nested state. However, the use of Immer does add some "magic", and Immer has its own nuances in behavior. You should read through pitfalls mentioned in the immer docs. Most importantly, you need to ensure that you either mutate the state argument or return a new state, but not both.

    From the pitfalls:

    Don't reassign the recipe argument

    Never reassign the draft argument (example: draft = myCoolNewState). Instead, either modify the draft or return a new state.

    So basically the anti-patterns are:

    • Mutating draft state object and returning a new state reference

      setCustomEquipment: (state, action) => {
        state.otherPropery = "foo";
      
        return {
          ...state,
          property: action.payload,
        };
      }
      
    • Reassignment of the current state draft object

      setCustomEquipment: (state, action) => {
        state = {
          ...state,
          otherPropery: "foo",
          property: action.payload,
        };
      }