Search code examples
javascriptreduxreact-reduxredux-toolkit

Why is my Redux Toolkit reducer mutating state?


So for some reason, it seems that the equipItemById reducer below is mutating state - despite it being basically verbatim from the Redux Toolkit example:

enter image description here

Full slice below:

import { createSlice } from '@reduxjs/toolkit';
import Item from '../../../Entities/Item/Item';

const initialState = {
  itemsById: [
    new Item('weapon', 'oak_stave', 0),
    new Item('chest', 'basic_robes', 0),
    new Item('feet', 'basic_boots', 0),
    new Item('head', 'basic_circlet', 0),
    new Item('consumable', 'potion_of_healing', 0),
    new Item('consumable', 'potion_of_healing', 1),
    new Item('consumable', 'potion_of_healing', 1),
    new Item('weapon', 'rusty_sword', 5),
    new Item('weapon', 'iron_sword', 5),
    new Item('weapon', 'steel_sword', 5),
    new Item('weapon', 'enchanted_steel_sword', 5),
  ],
  inTrade: false,
  actorInTradeById: undefined,
  itemsPlayerWantsToTradeById: [],
  itemsOtherActorWantsToTrade: [],
  itemsByLocationName: {
    centralSquare: [new Item('weapon', 'enchanted_steel_sword')],
  },
};

const itemSlice = createSlice({
  name: 'items',
  initialState: initialState,
  reducers: {
    addItemToActorFromLocationByIdAndName: (state, action) => {
      const { actorId, item } = action.payload;
      let itemCopy = item;
      item.ownerId = actorId;
      state.itemsById.push(itemCopy);
    },
    equipItemById: (state, action) => {
      const item = state.itemsById.find((item) => item.id === action.payload);
      item.equipped = true;
    },
    unequipItemById: (state, action) => {
      const { itemId } = action.payload;
      const item = state.itemsById.find((item) => item.id === itemId);
      item.equipped = false;
    },
    dropItemFromInventory: (state, action) => {
      const { itemId, locationName } = action.payload;
      const item = state.itemsById.find(
        (item) => item.id === itemId
      );
      item.ownerId = undefined;
      item.equipped = false;
      state.itemsById = state.itemsById.filter(item => item.id !== itemId);
      state.itemsByLocationName[locationName].push(item);
    },
    removeItemFromLocation: (state, action) => {
      const { itemId, locationName } = action.payload;
      state.itemsByLocationName[locationName] = state.itemsByLocationName[
        locationName
      ].filter((item) => item.id !== itemId);
    },
  },
});

export const {
  addItemToActorFromLocationByIdAndName: addItemToActorById,
  equipItemById,
  unequipItemFromActorByIds,
  inventorySetActiveItem,
  equippedSetActiveItem,
  dropItemFromInventory,
  dropItemFromEquipped,
  removeItemFromLocation,
} = itemSlice.actions;
export default itemSlice;

Dispatch is called as :

 dispatch(
    itemSlice.actions.equipItemById(props.item.id)
  );

And as you can see below - the action in question has the equipped property as the same both before and after - even though it's definitely false before.

enter image description here

an Item object simply looks like this (and is nested inside the itemsById array)

{
  '0': {
    type: 'weapon',
    id: 0,
    qty: 1,
    equipped: true,
    ownerId: 0,
    name: 'Oak Branch',
    icon: 'fa-staff',
    rarity: 0,
    descDetails: 'Deals an additional 1 damage per hit.',
    desc: 'A simple weapon. Strong and sturdy. No man turns down a stout wooden branch, when the alternative is an empty hand to face the sharp steel of a bandit.',
    stats: {},
    value: 1,
    slot: 'weapon_main',
    addedDmgString: '1'
  }
}

Solution

  • immer does not work on class instances by default. You can mark them as immerable though.

    But generally, you should not put class instances in your Redux store in the first place, see the Redux Style Guide