BioWorkWriting

Using socket.io with Vue and Vuex

  • JS
  • Websockets
  • Vue
  • Vuex

When building stagetimer.io I decided to use WebSockets to push updates to all connected devices. The app is built on top of Vue using Vuex for state management. For WebSockets, I am using the popular and easy to use socket.io library.

Contents

  1. The desired Result
  2. Existing Plugins
  3. Instantiating the Socket
  4. Using the Socket with Vue
  5. Using the Socket with Vuex

The desired Result

I want to use socket.io inside Vue components as well as in the Vuex store. It must also reuse the same socket connection in both cases.

Inside Vue components I want to have access to the socket instance through this.$socket and use it to listen to and emit messages like this:

export default {
  created () {
    this.$socket.on('message', this.receiveMessage)
  },
  destroyed () {
    this.$socket.removeAllListeners('message')
  }
  methods: {
    sendMessage (payload) {
      this.$socket.emit('message', payload)
    },
    receiveMessage (payload) {
      console.log('received a message', payload)
    },
  }
}

Inside the Vuex store I want to do the same, except that a received event should translate into a store action like so:

return new Vuex.Store({
  state: { ... },
  getters: { ... },
  mutations: { ... },
  actions: {
    sendMessage (context, payload) {
      this.$socket.emit('message', payload)
    },
    receiveMessage ({ commit }, payload) {
      commit('doSomethingWithMessage', payload)
    },
  },
})

Existing Plugins

There is no lack of socket.io plugins for Vue. vue-socket.io looks like a good solution. However, I try to avoid 3rd party dependencies if I can easily implement their function myself, as in the case of socket.io.

Instantiating the Socket

To reuse the socket I need to pass the same instance to Vue and the Vuex store. Basically, I want to import the same instance in two different files, kind of like a singleton. Fortunately, the »JavaScript specification guarantees that [I will] receive the same module instance« if I import the same path twice (Source). This means I can create a simple socket.js file that exports an instance of socket.io.

// socket.js
import io from 'socket.io-client'
const socket = io('http://localhost:3000')
export default socket

Using the Socket with Vue

Now I can import the socket.io instance into the default main.js file where Vue gets instantiated. To have this.$socket available in my Vue single file components I can simply inject the socket into the prototype of Vue.

// main.js
import Vue from 'vue'
import socket from './socket'

Vue.prototype.$socket = socket

new Vue({...})

Using the Socket with Vuex

With Vuex it’s not quite so simple. I need to write a little plugin to have this.$socket available inside store functions. Additionally, I have to define a mapping between socket events and the store actions they should trigger.

Here is the plugin code:

// websocketStorePlugin.js
export default function createWebSocketPlugin (socket) {
  return store => {
    store.$socket = socket
    socket.on('message', payload => store.dispatch('receiveMessage', payload))
  }
}

Instead of mapping socket events with store actions explicitly, I could also just map every event to a store action of the same name. But in my actual app, I like to keep it specific and I also like to use store modules.

I then import this plugin into my main store file, usually store.js or store/index.js. The createWebSocketPlugin method is called with the socket.io instance and passed to the store as a plugin.

import Vue from 'vue'
import Vuex from 'vuex'
import createWebSocketPlugin from './websocketStorePlugin'
import socket from '../socket'

Vue.use(Vuex)
const websocketPlugin = createWebSocketPlugin(socket)
export default new Vuex.Store({
  // ...
  plugins: [websocketPlugin],
})

And yeah, that’s it!