I have a project that utilizes Vue 3 (w/ Composition API), Pinia, Typescript, and SignalR. I'm running into an issue with calling a class instance method from within a pinia store action.
This is the problem line: currentObject.update(updatedObject) in ObjectStore.ts towards the bottom.
That line of code can usually run once without issue, but if it is rerun, I get this error: Uncaught (in promise) TypeError: currentObject.update is not a function.
I know it is because currentObject is no longer of type ObjectTracking because when I use currentObject instance of ObjectTracking, it returns false. However, the instances within allObjectTracking return true. I'm not sure where/how it would be "stripping" the class from currentObject. My only guess is that Pinia strips classes from the properties when managing state. This is just a guess, but I'm hoping to get a more definitive answer and explanation.
I know I could achieve the same functionality by moving the update logic into ObjectStore, but I feel like utilizing class instance methods is one of the benefits of using TypeScript. Is there a way to make this structure work, or should I always assume objects/properties used in a Pinia store are not instances of a class?
ObjectStore.ts
import { defineStore } from 'pinia'
import { getAllObjectTracking } from '@/api/services/objectTracking'
import ObjectTracking from '@/types/ObjectTracking'
export const useObjectTrackingStore = defineStore('ObjectTrackingStore', {
state: () => {
return {
allObjectTracking: [] as ObjectTracking[],
isInitialized: false,
}
},
actions: {
async initializeStore() {
try {
this.getObjectTracking()
this.isInitialized = true
} catch (error) {
console.error(error)
}
},
async getObjectTracking() {
try {
this.allObjectTracking = await getAllObjectTracking()
console.log(this.allObjectTracking[0] instanceof ObjectTracking) // Always true
} catch (error) {
console.error(error)
}
},
async updateObjectTracking(updatedObject: ObjectTracking) {
const currentObject = this.allObjectTracking.find((l) => l.id === updatedObject.id)
if (currentObject !== null && currentObject !== undefined) {
currentObject.update(updatedObject)
} else {
this.allObjectTracking.push(updatedObject)
}
},
},
/// getters
})
ObjectTracking.ts
import { Guid } from 'guid-typescript'
export interface IObjectTracking {
id: string
}
export default class ObjectTracking implements IObjectTracking {
id: string = Guid.createEmpty().toString()
constructor(instanceObject: ObjectTracking) {
this.update(instanceObject)
}
async update(updatedObject: ObjectTracking): Promise<void> {
this.id = updatedObject.id
}
}
objectTracking.ts
import apiClient from '../apiClient'
import ObjectTracking from '@/types/ObjectTracking'
const objectTrackingPath = { ** api path ** }
export async function getAllObjectTracking(): Promise<ObjectTracking[]> {
try {
const response = await apiClient.get<ObjectTracking[]>(objectTrackingPath)
if (response === null || response.data === null) {
return [] as ObjectTracking[]
} else {
const objectList = [] as ObjectTracking[]
for (const objectInstance in response.data) {
objectList.push(new ObjectTracking(response.data[objectInstance] as ObjectTracking))
}
return objectList
}
} catch (error) {
console.error(error)
return []
}
}
SignalRStore.ts
import { defineStore } from 'pinia'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import SignalRState from '@/types/SignalRState'
import { ref } from 'vue'
import { useObjectTrackingStore } from './ObjectStore'
const disconnectedClock = ref()
export const useSignalRStore = defineStore('SignalRStore', {
state: () => {
return {
state: { newState: 0, oldState: 0 } as SignalRState,
}
},
actions: {
async startSignalR() {
const signalRConnection = $.hubConnection({ ** Connection URL **})
signalRConnection.reconnecting(() => {
console.log('SignalR Reconnecting...')
})
signalRConnection.reconnected(() => {
console.log('SignalR Reconnected')
})
signalRConnection.disconnected(() => {
console.log('State: ' + JSON.stringify(this.state))
disconnectedClock.value = setInterval(function () {
console.log('Trying to start')
signalRConnection.start({ withCredentials: false })
}, 5000) // Restart connection after 5 seconds.
})
signalRConnection.stateChanged((state: SignalRState) => {
this.state = state
console.log('state changed: ' + JSON.stringify(state))
if (state.newState === 0 || state.newState === 1) {
clearInterval(disconnectedClock.value)
}
})
const objectTrackingStore = useObjectTrackingStore()
const signalRProxy = signalRConnection.createHubProxy('ObjectHub')
signalRProxy.on('updateObjectTracking', objectTrackingStore.updateObjectTracking)
await signalRConnection
.start({ withCredentials: false })
.done(() => {
console.log('Now connected')
})
.fail(() => {
console.log('Could not connect')
})
},
},
/// getters
getters: {
isConnected(): boolean {
return this.state.newState === 1 // 0 connecting, 1 - connected, 2 - reconnecting, 4 - disconnected
},
},
})
this.allObjectTracking.push(updatedLoad)is used to add the object to the list if it does not currently exist in the list. That part has been working fine. I think you're correct that it is related to Vue's reactivity system. I used a more OOP methodology when writing this code, which I'm now learning, Vue does not like. I've since switched the code to a functional programming format, and that has solved a lot of my issues.