Search code examples
vue.jswebsocketvuex

Websocket within Vuex module: Async issue when trying to use Vuex rootstate


I'm trying to populate my app with data coming from a websocket in the most modular way possible trying to use best practices etc. Which is hard because even when I have dig very deep for advice on the use of websockets / Vuex and Vue I still can't find a pattern to get this done. After going back and forth I have decided to use a store to manage the state of the websocket and then use that vuex module to populate the state of other components, basically a chat queue and a chat widget hence the need to use websockets for real time communication.

This is the websocket store. As you can see I'm transforming the processWebsocket function into a promise in order to use async/await in other module store actions. The way I see this working (and I'm prob wrong, so PLEASE feel free to correct me) is that all the components that will make use of the websocket module state will wait until the state is ready and then use it (this is not working at the moment):

export const namespaced = true
export const state = {
    connected: false,
    error: null,
    connectionId: '',
    statusCode: '',
    incomingChatInfo: [],
    remoteMessage: [],
    messageType: '',
    ws: null,
}
export const actions = {
    processWebsocket({ commit }) {
        return new Promise((resolve) => {
            const v = this
            this.ws = new WebSocket('xyz')
            this.ws.onopen = function (event) {
                commit('SET_CONNECTION', event.type)
                v.ws.send('message')
            }
            this.ws.onmessage = function (event) {
                commit('SET_REMOTE_DATA', event)
                resolve(event)
            }
            this.ws.onerror = function (event) {
                console.log('webSocket: on error: ', event)
            }
            this.ws.onclose = function (event) {
                console.log('webSocket: on close: ', event)
                commit('SET_CONNECTION')
                ws = null
                setTimeout(startWebsocket, 5000)
            }
        })
    },
}
export const mutations = {
    SET_REMOTE_DATA(state, remoteData) {
        const wsData = JSON.parse(remoteData.data)
        if (wsData.connectionId && wsData.connectionId !== state.connectionId) {
            state.connectionId = wsData.connectionId
            console.log(`Retrieving Connection ID ${state.connectionId}`)
        } else {
            state.messageType = wsData.type
            state.incomingChatInfo = wsData.documents
        }
    },
    SET_CONNECTION(state, message) {
        if (message == 'open') {
            state.connected = true
        } else state.connected = false
    },
    SET_ERROR(state, error) {
        state.error = error
    },
}

When I debug the app everything is working fine with the websocket store, I can see its state, the data from the server is there etc. The problem comes when I try to populate other components properties using the websocket. By the time other components need the websocket state this is not ready yet so I'm getting errors. Here's an example of one of my components trying to use the websocket state, I basically call an action from the created cycle method:

<template>
    <ul class="overflow-y-auto overflow-hidden pr-2">
        <BaseChat
            v-for="(chat, index) in sortingIncomingChats"
            :key="index"
            :chat="chat"
            :class="{ 'mt-0': index === 0, 'mt-4': index > 0 }"
        />
    </ul>
</template>

<script>
import { mapState } from 'vuex'
import BaseChat from '@/components/BaseChat.vue'
export default {
    components: {
        BaseChat,
    },
    created() {
        this.$store.dispatch('chatQueue/fetchChats')
    },
    data() {
        return {
            currentSort: 'timeInQueue',
            currentSortDir: 'desc',
            chats: [],
        }
    },
    computed: {
        sortingIncomingChats() {
            return this.incomingChats.slice().sort((a, b) => {
                let modifier = 1
                if (this.currentSortDir === 'desc') modifier = -1
                if (a[this.currentSort] < b[this.currentSort])
                    return -1 * modifier
                if (a[this.currentSort] > b[this.currentSort])
                    return 1 * modifier
                return 0
            })
        },
    },
}
</script>

This is the chatQueue Vuex module that have the fetchChats action to populate data from the websocket to the APP:

export const namespaced = true
export const state = () => ({
    incomingChats: [],
    error: '',
})
export const actions = {
    fetchChats({ commit, rootState }) {
        const data = rootState.websocket.incomingChats
        commit('SET_CHATS', data)
    },
}
export const mutations = {
    SET_CHATS(state, data) {
        state.incomingChats = data
    },
    SET_ERROR(state, error) {
        state.incomingChats = error
        console.log(error)
    },
}

This is where I get errors because "rootState.websocket.incomingChats" is not there yet when its called by the fetchChats module action, so I get:

TypeError: Cannot read properties of undefined (reading 'slice')

I have tried to transform that action into an async / await one but it's not working either, but as I mentioned I'm really new to async/await so maybe I'm doing something wrong here:

async fetchChats({ commit, rootState }) {
    const data = await rootState.websocket.incomingChats
    commit('SET_CHATS', data)
},

Any help will be really appreciated.


Solution

  • In case somebody have the same problem what I ended up doing is adding a getter to my websocket module:

    export const getters = {
        incomingChats: (state) => {
            return state.incomingChatInfo
        },
    }
    

    And then using that getter within a computed value in the component I need to populate with the websocket component.

    computed: {
        ...mapGetters('websocket', ['incomingChats']),
    },
    

    And I use the getter on a regular v-for loop within the component:

    <BaseChat
        v-for="(chat, index) in incomingChats"
        :key="index"
        :chat="chat"
        :class="{ 'mt-0': index === 0, 'mt-4': index > 0 }"
    />
    

    That way I don't have any kind of sync problem with the websocket since I'm sure the getter will bring data to the component before it tries to use it.