Does Immer allow us to store class instances in a redux reducer?

Viewed 976

Redux Style-Guide states:

Avoid putting non-serializable values such as Promises, Symbols, Maps/Sets, functions, or class instances into the Redux store state or dispatched actions. This ensures that capabilities such as debugging via the Redux DevTools will work as expected. It also ensures that the UI will update as expected.

Immer - Complex-Objects

Every other object must use the immerable symbol to mark itself as compatible with Immer. When one of these objects is mutated within a producer, its prototype is preserved between copies.


class Foo {
    [immerable] = true // Option 1

    constructor() {
        this[immerable] = true // Option 2
    }
}

Foo[immerable] = true // Option 3

I know I can have a class instance reducer...but should I?

I'm assuming the dangers of this is that time-traveling & Redux DevTools may be broken - but immer will protect against any mutations outside the reducer.

Example:

import { immerable } from "immer";

class MyClass {
  constructor() {
    this[immerable] = true;
    this.data = [];
  }

  addData(row = "") {
    this.data.push(row);
  }

  prettyPrint() {
    this.data.map((txt, index) => console.log(`Row ${index}: ${txt}`));
  }
}
const example = createSlice({
  name: "example",
  initialState: new MyClass(),
  reducers: {
    addItem: (state, action) => {
      state.addData(action.payload);
    }
  }
});

let state = example.reducer(undefined, example.actions.addItem("Test"));
console.log(state.prettyPrint());

state = example.reducer(state, example.actions.addItem("Test Me too"));
console.log(state.prettyPrint());

console.log(state);

//Will error
// state.addData('Will Error');

The reason for doing this? We have complex business logic inside large classes. Like in the example, prettyPrint, we have some complex functionality that is encapsulated in the class instance that we use across the react apps.

My other idea for the approach is to instantiate and serialize inside the reducer so that the json representation of our class instances are only ever stored inside the reducers. If I can avoid having to perform instantiate -> serialize every action though, and have access to utility functions on the state tree, this would be preferred.

2 Answers

You should not use a class at all for this.

Just store your data as a JS Object or array directly and use selectors to do the pretty print.

it's definitely not recomanded to use class, but I spend some time experimenting anyway. and for now I could make it work but I dont know how it will play out in long term.

making class for initstate with bunch of getters and setter work fine in my Slice and reducers work how i would expect...

class GameCore {
    [immerable] = true
                                            //score
    _score = new Score(0, this.numBase, this.charFrames)
    get score() {
        return this._score
    };
    set score(value) {
        this._score.value = value
    };
                                            //combo
    _combo = 0;
    _combo_min = 0
    _combo_max = 100
    get combo() {
        return this._combo;
    };
    _combo_setter(value) {
        const min = this._combo_min;
        const max = this._combo_max;
        if (value >= min && value <= max) { return value }
        if (value <= min) {return min}
        return max

    };
    set combo(value) {
        this._combo = this._combo_setter(value);
    };
                                            //numBase
    _numBase = 10
    get numBase() {
        return this._numBase
    };
                                            //charFrames
    _charFrames = 3
    get charFrames() {
        return this._charFrames
    };
                                            //buttonActive
    _buttonActive = true
    get buttonActive() {
        return this._buttonActive
    };
    set buttonActive(value) {
        this._buttonActive = value ? true : false
    };
                                            //combOverflow
    _combOverflow = false
    get combOberflow() {
        return this._combOverflow
    };
    set combOberflow(value) {
        if (value) {
            this._combOverflow = true
        } else {
            this._combOverflow = true
        }

    };
                                            //overflowDuration
    _overflowDuration = 5
    get overflowDuration() {
        return this._overflowDuration
    };
                                            //clickingLimit
    _clickingLimit = 24/1000
    get clickingLimit() {
        return this._clickingLimit
    };
                                            //clickPower
    _clickPower = 1
    get clickPower() {
        return this._clickPower
    };
                                            //comboPower
    _comboPower = 1
    get comboPower() {
        return this._comboPower
    };

}

const ClickerCore = new GameCore();
export default ClickerCore;
const initialState = ClickerCore

const clickerSlice = createSlice({
  name: "clicker",
  initialState,
  reducers: {
    reduceCombo: (state, { payload }) => {
      state.combo -= payload;
    },

    overScored: () => {},

    overflowOn: (state) => {
      state.combOverflow = true;
    },

    overflowOff: (state) => {
      state.combOverflow = false;
    },

    buttonClick: () => {},

    processClick: (state) => {
      state.buttonActive = true;
      state.combo += state.comboPower;
      state.score.value += state.clickPower;
    },

    clickingStop: (state) => {
      state.buttonActive = false;
    },
  },
});

I havent tested using class methods yet but for them I think I would need to experiment with immer {produce} but scratch taht i havent done it yet ...

in conclusion: if You would want to work with class You propably would have to design it specially for this case from scratch and just adding [immerable] = true will not be sufficient. Also I am not sure if it is worth it anyway beside of some more semi control, it was both pain and fun thou

Related