1

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
ohthepain
  • 2,004
  • 3
  • 19
  • 30

1 Answers1

1

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.

phry
  • 35,762
  • 5
  • 67
  • 81