Search code examples
react-reduxredux-toolkitreact-tsx

React Redux Toolkit TS - Can't access class methods from my state objects


I have set up my redux store and I am able to edit my state objects and get their values. But for some reason I can't call methods on the state objects. It's as if they are stored as javascript objects.

When I call getSequence() the first console.log correctly logs the sequence structure. But the second log call gives me an error

sequence.dump is not a function

Here is my store, including getSequence():

import {configureStore} from '@reduxjs/toolkit'
import sequenceReducer, {selectSequence} from '../feature/sequence-slice'
import midiMapsReducer from '../feature/midimaps-slice'
import {Sequence} from "../player/sequence";

export function getSequence() : Sequence {
    const sequence: Sequence = selectSequence(store.getState())
    console.log(`getCurrentSequence: ${JSON.stringify(sequence)}`)
    console.log(`getCurrentSequence: ${sequence.dump()}`)
    return sequence
}

const store = configureStore({
    reducer: {
        sequence: sequenceReducer,
        midiMaps: midiMapsReducer,
    }
})

export default store

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

I set up my slice state like this:

interface SequenceSliceState {
    value: Sequence;
}
const initialState : SequenceSliceState = {
    value: new Sequence({})
}
export const sequenceSlice = createSlice({
    name: 'sequence',
    // type is inferred. now the contained sequence is state.value
    initialState,
    reducers: {

and here is my Sequence module:

export class SequenceStep {
    time: number = 0;
    note: number = 64;
    velocity: number = 100;
    gateLength: number = 0.8;
}

export class MidiSettings {
    midiInputDeviceId: string = "";
    midiInputDeviceName: string = "";
    midiInputChannelNum: number = -1;
    midiOutputDeviceId: string = "";
    midiOutputDeviceName: string = "";
    midiOutputChannelNum: number = 0;
}

export class EnvelopePoint {
    time: number = 0;
    value: number = 0;
}

export class Envelope {
    id: string = "";
    controller: string = "";
    points: Array<EnvelopePoint> = [];
    locked: boolean = true;
    mode: string = "loop";
    trigger: string = "first";
    type: string = "envelope";
    // cacheMinValue: number = 0;
    // cacheMaxValue: number = 127;

    constructor(fake: any) {
        this.id = fake.id;
        this.controller = fake.controller;
        this.points = fake.points;
        this.locked = fake.locked;
        this.mode = fake.mode;
        this.trigger = fake.trigger;
        this.type = fake.type;
    }

    dump() : string {
        return "I am an envelope"
    }

    getValue(time: number) : number {
        const numpoints = this.points.length
        const length: number = this.points[numpoints - 1].time;
        const loop: boolean = true;
        const position: number = time % length;
        var index = 0
        while (index < numpoints && this.points[index].time < position) {
            ++index
        }

        if (index == 0) {
            return this.points[0].value
        } else if (index >= numpoints) {
            return this.points[numpoints - 1].value
        } else {
            const p0: EnvelopePoint = this.points[index - 1];
            const p1: EnvelopePoint = this.points[index];
            if (p0.time == p1.time) {
                return p0.value
            } else {
                return p0.value + (position - p0.time) / (p1.time - p0.time) * (p1.value - p0.value)
            }
        }
    }
}

export class Sequence {
    _id: string = "";
    name: string = "";
    text: string = "";
    user_name: string = "";
    user_id: string = "";
    steps: Array<SequenceStep> = []
    tempo: number = 120.0;
    length: number = 8;
    numSteps: number = 8;
    division: number = 8;
    midiSettings: MidiSettings = new MidiSettings();
    currentEnvelopeId: string = "";
    envelopes: Array<Envelope> = []

    constructor(fakeSequence: any) {
        this._id = fakeSequence._id;
        this.name = fakeSequence.name;
        this.text = fakeSequence.text;
        this.user_id = fakeSequence.user_id;
        this.steps = fakeSequence.steps;
        this.tempo = fakeSequence.tempo;
        this.length = fakeSequence.length;
        this.numSteps = fakeSequence.numSteps;
        this.division = fakeSequence.division;
        this.midiSettings = fakeSequence.midiSettings;
        this.currentEnvelopeId = fakeSequence.currentEnvelopeId;
        this.envelopes = new Array<Envelope>();
        if (fakeSequence.envelopes) {
            for (const fakeEnvelope in fakeSequence.envelopes) {
                this.envelopes.push(new Envelope(fakeEnvelope));
            }
        }
    }

    dump() : string {
        return "I am a Sequence"
    }
}

Here is the rest of my sequence slice:

import { createSlice } from '@reduxjs/toolkit'
import { v4 as uuidv4 } from "uuid";
import {Envelope, Sequence} from "../player/sequence"
import {RootState} from "../app/store";

interface SequenceSliceState {
    value: Sequence;
}

const initialState : SequenceSliceState = {
    value: new Sequence({})
}

export const sequenceSlice = createSlice({
    name: 'sequence',
    // type is inferred. now the contained sequence is state.value
    initialState,
    reducers: {
        sequenceLoad: (state, payloadAction) => {
            console.log(`🍕sequencerSlice.sequenceLoad ${JSON.stringify(payloadAction)}`)
            state.value = JSON.parse(JSON.stringify(payloadAction.payload.sequence));
            return state;
        },
        sequenceName: (state, payloadAction) => {
            console.log(`🍕sequencerSlice.sequenceName ${JSON.stringify(payloadAction)}`)
            state.value.name = payloadAction.payload;
            return state
        },
        numSteps: (state, payloadAction) => {
            var sequence: Sequence = state.value
            sequence.numSteps = payloadAction.payload;

            console.log(`🍕hi from numsteps ${sequence.numSteps} ${sequence.steps.length}`);
            if (sequence.numSteps > sequence.steps.length) {
                console.log(`🍕extend sequence`);
                var newSteps: any = [];
                for (var n = sequence.steps.length + 1; n <= sequence.numSteps; n++) {
                    newSteps = newSteps.concat({ note: 60, velocity: 100, gateLength: 0.9, });
                    console.log(`added step - now ${newSteps.length} steps`)
                }

                console.log(`🍕handleNumStepsChange: ${newSteps.length} steps -> ${newSteps}`)
                const newStepsArray = sequence.steps.concat(newSteps);
                sequence.steps = newStepsArray;
                // console.log(`🍕handleNumStepsChange: ${stepsEdits.length} steps -> ${stepsEdits}`)
            }
            return state
        },
        midiSettings: (state, payloadAction) => {
            console.log(`🍕sequence-slice - payloadAction ${JSON.stringify(payloadAction)}`)
            console.log(`🍕sequence-slice - midiSettings ${JSON.stringify(payloadAction.payload)}`)
            state.value.midiSettings = payloadAction.payload;
            return state
        },
        division: (state, payloadAction) => {
            state.value.division = payloadAction.payload;
            return state
        },
        length: (state, payloadAction) => {
            state.value.length = payloadAction.payload;
            return state
        },
        sequenceText: (state, payloadAction) => {
            state.value.text = payloadAction.payload;
            return state
        },
        tempo: (state, payloadAction) => {
            console.log(`🍕Edit tempo payloadaction ${JSON.stringify(payloadAction)}`)
            state.value.tempo = payloadAction.payload
            // return { ...state, tempo: parseInt(payloadAction.payload) }
            // state.tempo = payloadAction.payload
            return state
        },
        stepControllerValue: (state: any, payloadAction) => {
            var sequence: Sequence = state.value
            console.log(`🍕Edit stepControllerValue ${JSON.stringify(payloadAction)}`)
            const stepNum: number = payloadAction.payload.stepNum
            const controllerNum: number = payloadAction.payload.controllerNum
            const controllerValue: number = payloadAction.payload.controllerNum
            // sequence.steps[stepNum][controllerNum] = controllerValue;
            // sequence.steps = steps
            return state
        },
        stepNote: (state, action) => {
            const stepnum = action.payload.stepNum
            const notenum = action.payload.note
            console.log(`🍕sequence-slice.stepNote ${JSON.stringify(action.payload)} ${stepnum} ${notenum}`)
            var sequence: Sequence = state.value
            // var steps: Array<SequenceStep> = [...sequence.steps];
            sequence.steps[stepnum].note = notenum
            // sequence.steps = steps;
            return state
        },
        stepGateLength: (state, action) => {
            const stepnum = action.payload.stepNum
            const gateLength = action.payload.gateLength
            // console.log(`🍕sequence-slice.stepNote ${JSON.stringify(action.payload)} ${stepnum} ${notenum}`)
            // var steps = [...state.steps];
            var sequence: Sequence = state.value
            sequence.steps[stepnum].gateLength = gateLength
            // state.steps = steps;
            return state
        },
        stepVelocity: (state, action) => {
            const stepnum = action.payload.stepNum
            const velocity = action.payload.velocity
            // console.log(`🍕sequence-slice.stepNote ${JSON.stringify(action.payload)} ${stepnum} ${notenum}`)
            var sequence: Sequence = state.value
            // var steps = [...state.steps];
            sequence.steps[stepnum].velocity = velocity
            // state.steps = steps;
            return state
        },
        decrement: state => {
            var sequence: Sequence = state.value
            sequence.numSteps -= 1;
            return state
        },
        // incrementByAmount: (state, action) => {
        //     var sequence: Sequence = state.value
        //     sequence.numSteps += action.payload;
        //     return state
        // },
        createEnvelope: (state, action) => {
            console.log(`🍕sequenceSlice - createEnvelope: action should be controller ${JSON.stringify(action)}`)
            const controller = action.payload.controller
            console.log(`🍕sequenceSlice - createEnvelope: controller ${JSON.stringify(controller)}`)
            var sequence: Sequence = state.value
            var newEnvelopeId = uuidv4()
            var newEnvelope = new Envelope({
                id: newEnvelopeId,
                controller: controller.name,
                points: [{ time: 0, value: 0}, ],
                locked: true,
                mode: "loop",
                trigger: "first",
                type: "envelope"
            })

            if (sequence.envelopes == null) {
                sequence.envelopes = new Array<Envelope>();
            }

            sequence.envelopes = [...sequence.envelopes, newEnvelope];
            sequence.currentEnvelopeId = newEnvelopeId;

            // console.log(`🍕state.envelopes <${state.envelopes}> (added ${newEnvelopeId}`);
            return state
        },
        envelopeValue: (state, action) => {
            console.log(`🍕sequenceSlice - envelopeValue: action ${JSON.stringify(action)}`)
            const ccValue = action.payload.value
            const controller = action.payload.controller
            const envelopeId = action.payload.envelopeId
            var sequence: Sequence = state.value
            var envelope = sequence.envelopes.find((envelope: any) => envelope.id === envelopeId);
            if (envelope) {
                console.log(`🍕envelope ${envelopeId} ${JSON.stringify(envelope)}`)
                const ccid = action.payload.ccid

                // const currentValue = envelope.points[0].value
                // const currentLsb = currentValue % ((controller.max + 1) / 128)
                // const currentMsb = currentValue - currentLsb

                // const value = action.payload.value * ((controller.max + 1) / 128)
                envelope.points[0] = {time: 0, value: action.payload.value}
            }
            return state
        },
        currentEnvelopeId: (state, action) => {
            console.log(`🍕sequence-slice: action ${JSON.stringify(action)}`)
            var sequence: Sequence = state.value
            console.log(`🍕sequence-slice: currentEnvelopeId - was ${sequence.currentEnvelopeId}`);
            sequence.currentEnvelopeId = action.payload.envelopeId;
            console.log(`🍕sequence-slice: currentEnvelopeId - now ${sequence.currentEnvelopeId}`);
            return state
        },
        addEnvelopePoint(state, action) {
            console.log(`addEnvelopePoint: action ${JSON.stringify(action)}`)
            const envelopeId = action.payload.envelopeId
            var sequence: Sequence = state.value
            var envelope = sequence.envelopes.find((envelope: any) => envelope.id === envelopeId);
            if (envelope) {
                envelope.points.push({time: action.payload.time, value: action.payload.value})
                envelope.points = envelope.points.sort((a,b) => { return a.time - b.time })
                console.log(`addEnvelopePoint: found envelope. Points are now ${JSON.stringify(envelope.points)}`)
            }
            return state
        },
        deleteEnvelopePoint(state, action) {
            console.log(`deleteEnvelopePoint: point ${JSON.stringify(action.payload)} ${action.payload.envelopeId}`)
            const envelopeId = action.payload.envelopeId
            var sequence: Sequence = state.value
            var envelope = sequence.envelopes.find((envelope: any) => envelope.id === envelopeId);
            if (envelope) {
                console.log(`deleteEnvelopePoint: envelope ${JSON.stringify(envelope)} ${envelope.points.length}`)
                for (var n = 0; n < envelope.points.length; n++) {
                    console.log(`envelope.points[n] ${JSON.stringify(envelope.points[n])} == action.payload.point ${JSON.stringify(action.payload.point)}`)
                    if (envelope.points[n].time == action.payload.point.time && envelope.points[n].value == action.payload.point.value) {
                        envelope.points.splice(n, 1)
                        console.log('deleteEnvelopePoint: found it')
                        console.log(`deleteEnvelopePoint: envelope ${JSON.stringify(envelope)} ${envelope.points.length}`)
                        break;
                    }
                }
            }
            return state
        },
        moveEnvelopePoint(state, action) {
            console.log(`moveEnvelopePoint: point ${JSON.stringify(action.payload)} ${action.payload.envelopeId}`)
            console.log(`moveEnvelopePoint: point ${JSON.stringify(action)}`)
            const envelopeId = action.payload.envelopeId
            const pointNum : number = action.payload.pointNum
            const time : number = action.payload.time
            const value : number = action.payload.value
            var sequence: Sequence = state.value
            var envelope = sequence.envelopes.find((envelope: any) => envelope.id === envelopeId);
            if (envelope) {
                console.log(`moveEnvelopePoint: envelope ${JSON.stringify(envelope)} point ${pointNum} -> ${time},${value}`)
                envelope.points[pointNum].time = time
                envelope.points[pointNum].value = value
            }
            return state
        }
    }
})

export const {
    sequenceLoad,
    sequenceName,
    numSteps,
    midiSettings,
    division,
    sequenceText,
    length,
    tempo,
    stepControllerValue,
    stepNote,
    stepGateLength,
    stepVelocity,
    decrement,
    envelopeValue,
    addEnvelopePoint,
    deleteEnvelopePoint,
    currentEnvelopeId,
    moveEnvelopePoint,
} = sequenceSlice.actions

export const selectSequence = (state: RootState) => state.sequence.value

export default sequenceSlice.reducer

Solution

  • The moment you call JSON.parse(JSON.stringify(payloadAction.payload.sequence)), you create a normal JavaScript object that just has the properties of the class instance, but not the functionality.

    Generally, you should not be storing things like class instances in a Redux store - classes cannot be serialized (as you just saw here), which causes problems with the devtools and libraries like redux-persist. Also, they tend to modify themselves, which collides with the core tenets of Redux.

    Store pure data instead, use reducers to do modifications and selectors to derive further data from it.