Real-Time Data Transfer Using Vue and Socket.IO [Part 2 of 3]

Blog / Rob Hyndman / December 10, 2020

This project uses Node.js v14.15.1.

In the last post we covered setting up a simple server and client which communicate with each other using websockets. This post will focus on the creation of a chat room. The third and final post in this series will build upon the techniques that we’ll cover in this post to create a simple multiplayer game “Fill Frenzy!”.

Add Real-Time Chat

The first of the two main components we will cover will be the chat room. The chat room will have the following features:

  • Support for multiple users,
  • A list of all connected users,
  • A history of all chat messages, and
  • Notifications for when other users are typing.
Fig.1 – A preview of the chat component

The chat component will be lengthy and touch multiple files, so I will break it down into manageable sections as best I can.

Update the Server Code

Having all the server code in one file will become painful to manage, especially since we’ll be adding new code to handle more components of our app. Let’s try to structure it a little better, and then add our chat component.

  1. Create a new folder named server in the project root.
  2. Move server.js into the server folder, then rename it to index.js.
  3. Create server/chat.js and add the following code:
  4. let socketio = undefined;
    
    const chatHistory = [];
    const userList = [];
    
    const userTypingStatus = {};
    const timers = {};
    const typingStatusTime = 2000;
    
    const id = {
      message: 0,
      user: 0,
      unique: {}
    };

    Firstly, we create a variable socketio to use as a reference to our main Socket.IO object, and arrays to contain our chat messages and list of users. Next, we add some properties to keep track of the User is typing notifications. We also create an object to keep track of the IDs that we’ll attach to messages and users.

    const initialise = (io) => {
      socketio = io;
    }

    Next we create an initialise function which we can use to set our socketio reference.

    const run = (socket) => {
      // new socket connected, send user list and chat history
      socket.emit('userList', userList);
      socket.emit('chatHistory', chatHistory);
    
      handleUserConnected(socket);
      handleChatMessage(socket);
      handleTypingStatus(socket);
    }

    The run function will handle the main functionality of the chat room for each individual socket which is passed to the function as an argument. The function will be called only once per socket when a new connection is made, so we will take advantage of this to send the current state of the app to the socket by emitting the events userList and chatHistory which both pass the corresponding information.

    const handleUserConnected = (socket) => {
      // listen for 'userConnected' events
      socket.on('userConnected', (username) => {
        // create a new user object
        let newUser = {
          userID: getID('user'),
          uniqueID: getID('unique', username),
          name: username
        };
    
        // determine user unique name
        newUser.uniqueName =
          newUser.uniqueID > 0 ?
            `${newUser.name}#${newUser.uniqueID}` :
            newUser.name;
    
        // add the user to the userList
        userList.push(newUser);
    
        // send a login event to this user
        socket.emit('userLogin', newUser);
    
        // create a new message about the connection
        let connectedMessage = {
          sender: 'Server',
          text: `${newUser.uniqueName} connected`,
          id: getID('message'),
          time: Date.now()
        };
    
        // add the message to the chatHistory
        chatHistory.push(connectedMessage);
    
        // send the user connection to all connected sockets
        socketio.emit('userConnected', newUser);
        socketio.emit('chatMessage', connectedMessage);
      });
    }

    The handleUserConnected function listens for the userConnected event, and will respond appropriately.

    First, we create a new user object and determine what the unique version of the username would be. For example, if two users named Rob connected, one user would keep the username Rob while the other would change to Rob#1.

    Once the user is created, we store it in the userList then send an event notifying the user that they have now logged in. This event also sends the user object, so that the client knows which user it is, and has all the relevant information.

    Next, we create a chat message that will notify all users of the new connection and update the clients with this new information.

    You may have noticed that in this code snippet events were emitted using the socketio object, while in the previous snippet emitted events used the socket object. It is important to note the difference—socket.emit() is sending an event to that specific socket, whereas socketio.emit() is sending an event to all connected sockets, much like a broadcast.

    const handleChatMessage = (socket) => {
      // listen for 'chatMessage' events
      socket.on('chatMessage', (chatMessage) => {
        // attach a message id and timestamp
        chatMessage.id = getID('message');
        chatMessage.time = Date.now();
    
        // add the message to the chat history
        chatHistory.push(chatMessage);
    
        // clear typing status for this user
        clearTypingStatus(chatMessage.sender.userID);
    
        // send the message to all connected sockets
        socketio.emit('chatMessage', chatMessage);
      });
    }

    The handleChatMessage function listens for the chatMessage event, and will respond appropriately. We receive a chat message from the client, add an ID and timestamp, then save it to our chat history. Then we ensure that we clear the typing status so that clients stop seeing this user as typing a message. Lastly, update all clients with the new message.

    const handleTypingStatus = (socket) => {
      // listen for 'setTypingStatus' events
      socket.on('setTypingStatus', (typingStatus) => {
        // set typing status to true for this user
        userTypingStatus[typingStatus.user.userID] = {
          user: typingStatus.user,
          typing: true
        };
    
        // set a timer to reset the typing status
        setStatusTimer(typingStatus.user.userID);
    
        // broadcast the typing status'
        socketio.emit('typingStatus', userTypingStatus);
      });
    }

    The handleTypingStatus function listens for the setTypingStatus event, and will respond appropriately. We receive an event from the client, then set the typing status of the user to true and store that within an object using the user id as a key.

    Next we start a timer that will revert the typing status back to false after a certain amount of time. Lastly, we broadcast the current status of all our users, including the newly updated one.

    const clearTypingStatus = (userID) => {
      // set typing status to false for this user
      userTypingStatus[userID].typing = false;
    
      // if a timer exists for this user, remove it
      removeStatusTimer(userID);
    
      // broadcast the typing status'
      socketio.emit('typingStatus', userTypingStatus);
    }
    
    const removeStatusTimer = (userID) => {
      // if this user id is a key of timers, clear the timer
      if (timers.hasOwnProperty(userID)) {
        clearTimeout(timers[userID]);
      }
    }
    
    const setStatusTimer = (userID) => {
      // if a timer exists for this user, remove it
      removeStatusTimer(userID);
    
      // set a timer to clear the typing status
      timers[userID] = setTimeout(() => {
        userTypingStatus[userID].typing = false;
    
        // broadcast the typing status'
        socketio.emit('typingStatus', userTypingStatus);
      }, typingStatusTime);
    }

    These three functions assist in managing the typing status and associated timers of each user. The function clearTypingStatus sets the status to false and removes the timer immediately, whereas the function setStatusTimer sets the status to false after typingStatusTime time has elapsed.

    const getID = (type, username = undefined) => {
      let newID;
    
      if (username) {
        if (!id[type].hasOwnProperty(username)) {
          // this is a new username
          newID = id[type][username] = 0;
        } else {
          // this is a duplicate username
          newID = id[type][username];
        }
    
        id[type][username] += 1;
      } else {
        // return the next id
        newID = id[type];
        id[type] += 1;
      }
    
      return newID;
    }
    
    // export these functions for external use
    module.exports = { initialise, run };

    The getID function manages the distribution of IDs for our messages and users. Finally, we export the two functions that we want to access from our main server/index.js file.

  5. Update server/index.js to include our new chat functionality, replacing the code we wrote in part 1 of this series.
  6. const express = require('express');
    const http = require('http').Server(express);
    const socketio = require('socket.io')(http, { pingTimeout: 60000 });
    const chat = require('./chat');
    const port = 3030;
    
    chat.initialise(socketio);
    
    socketio.on('connection', (socket) => {
      // new socket connected
      chat.run(socket);
    });
    
    http.listen(port, () => {
      console.log('Server started on port', port);
    });

    After including the chat.js file we created, we pass the socketio instance to the initialise function. Then, when the connection event triggers we pass the new socket connection through to the run function.

Update the Client Functionality

Now that the server has been updated to handle our chat component, we need to update the client to store our chat data and respond to server events.

  1. Create src/store/chat.js and add the following code.
  2. This is the Vuex store for our chat room component. The store is a managed collection of data that we use as the middleman between the app front-end and the server. The front-end tells the store to send an event through the socket connection. When the app receives an event from the server through the socket connection, the store is updated with the new data. The app front-end only updates when there is new data in the store.

    import Vue from 'vue';
    
    export default {
      strict: false,
      namespaced: true,
      state: () => ({
        users: [],
        history: []
      }),

    state is the data that the store is holding. In this case we have users and history being kept in the store.

      getters: {
        GET_USERS: (state) => {
          return state.users;
        },
        GET_HISTORY: (state) => {
          return state.history;
        }
      },

    The front-end uses getters to retrieve data from the store.

      mutations: {
        SET_USERS(state, users) {
          state.users = users;
        },
        ADD_USER(state, user) {
          Vue.set(state.users, user.userID, user);
        },
        SET_HISTORY(state, history) {
          state.history = history;
        },
        ADD_MESSAGE(state, message) {
          Vue.set(state.history, message.id, message);
        }
      },

    mutations are used to change the data in the store somehow. For example, we can set an object collection entirely using SET_HISTORY, or add a new element to a collection with ADD_USER.

      actions: {
        RECEIVE_USERS({ commit }, users) {
          commit('SET_USERS', users);
        },
        RECEIVE_USER({ commit }, user) {
          commit('ADD_USER', user);
        },
        RECEIVE_HISTORY({ commit }, history) {
          commit('SET_HISTORY', history);
        },
        RECEIVE_MESSAGE({ commit }, message) {
          commit('ADD_MESSAGE', message);
        }
      }
    };

    actions are functions that can be called from the app front-end to perform a task, such as sending an event or receiving data. In our case, we would be calling an action from the front-end which in turn mutates our data in some fashion.

  3. Open src/store/index.js and edit the code until you have the following.
  4. import Vue from 'vue';
    import Vuex from 'vuex';
    import * as socketio from '../plugins/socketio';
    import chat from './chat';
    
    Vue.use(Vuex);
    
    export default new Vuex.Store({
      strict: false,
      actions: {
        SEND_EVENT({}, event) {
          socketio.sendEvent(event);
        }
      },
      modules: {
        chat
      }
    });

    Loading our new file into the store as a module allows us to maintain good object separation and the usage of namespaces to access particular parts of the store. We also added a SEND_EVENT action in this file, as all of our future components will need to send information to the server.

  5. Create src/mixins/addEventListener.js and add the following code.
  6. import * as socketio from '../plugins/socketio';
    
    export const addEventListener = {
      methods: {
        addEventListener(eventType, eventCallback) {
          socketio.addEventListener({
            type: eventType,
            callback: eventCallback
          });
        }
      }
    };

    Mixins are essentially a reusable code snippet that can be loaded into any of our future components without the need to rewrite the code. This mixin defines a function addEventListener which allows our components to specify which server events they will listen for, and how to react.

Create Chat Components

That takes care of the app-specific functionality. Now we can jump into building the front-end for our chat room. How should we tackle this? First let’s lay out the structure that we are aiming for. We’ll make it so that the app asks you to connect with a username before you begin, and once you are connected, you’ll see the chat room with components in this order:

  • User Login
  • Chat log
    • Messages
    • Typing notifications
  • Send a message
    • Text field
    • Send button
  • List of connected users
    • User chips

User Login

When a user first loads the app, they’ll be confronted with a simple login field for them to select a username to enter the chatroom with.

  1. Create src/components/ChatLogin.vue and add the following code:
  2. <template>
      <v-card-text>
        <v-text-field
          v-model="username"
          placeholder="Enter a username"
          @keydown.enter="setUsername()"
        ></v-text-field>
        <v-btn block @click="setUsername()">
          Connect
        </v-btn>
      </v-card-text>
    </template>
    
    <script>
    export default {
      name: 'ChatLogin',
      data: () => ({
        username: ''
      }),
      methods: {
        setUsername() {
          this.$emit('set-username', this.username);
        }
      }
    }
    </script>

    This component contains a text field which takes the desired username, and handles emitting the data to the parent component.

New Dependencies

We can’t have a really nice chat room without some scrolling functionality. I think it would be ideal for the chat log to automatically scroll down when a new message arrives, but it would be annoying if you were manually scrolling back up to read an old message when that happened.

Well, luckily we can save time and use an existing library to handle this for us! A bit of searching online found this nice little package called VueChatScroll.

  1. Install VueChatScroll:
  2. npm install --save vue-chat-scroll
  3. Import into src/main.js:
  4. import Vue from 'vue';
    import App from './App.vue';
    import store from './store';
    import vuetify from './plugins/vuetify';
    import moment from 'moment';
    import VueChatScroll from 'vue-chat-scroll';
    
    Vue.use(VueChatScroll);
    
    Vue.config.productionTip = false;
    Vue.prototype.moment = moment;
    
    new Vue({
      store,
      vuetify,
      render: (h) => h(App)
    }).$mount('#app');

Chat Log

The chat log will consist of messages and user typing notifications. It will be easiest if we work from the bottom up, so let’s start with individual messages. The easiest way to create a nice looking chat message is probably to use the v-alert component from Vuetify as a base and change up some settings.

  1. Create src/components/ChatMessage.vue and add the following code.
  2. <template>
      <v-alert
        class="my-1"
        dense
        outlined
        text
        max-width="80%"
        :icon="false"
        :border="getSetting('border')"
        :class="getSetting('align')"
        :type="getSetting('type')"
      >
        <div v-if="message.showSender" class="overline">
          <b>{{ message.sender.uniqueName }}</b>
        </div>
        <div class="black--text">
          {{ message.text }}
        </div>
        <div class="text-right time">
          {{ formatTime(message.time) }}
        </div>
      </v-alert>
    </template>

    First up, we’ll create a template that uses the v-alert component. We’ll get some easy wins by setting the props to style the message component. The message will contain text and a timestamp, and show the sender name in some circumstances.

    <script>
    export default {
      name: 'ChatMessage',
      props: {
        message: {
          type: Object,
          required: true
        }
      },
      data: () => ({
        messageSettings: {
          received: {
            align: 'align-self-start',
            border: 'left',
            type: 'error'
          },
          sent: {
            align: 'align-self-end',
            border: 'right',
            type: 'info'
          },
          server: {
            align: 'align-self-center',
            border: 'left',
            type: 'warning'
          }
        }
      }),

    The message component takes a message object as a prop, and can have three types – sent, received, and server messages.

      methods: {
        getSetting(setting) {
          return this.messageSettings[this.message.type][setting];
        },
        formatTime(time) {
          if (this.moment(time).isSame(this.moment(), 'day')) {
            return this.moment(time).format('h:mm a');
          } else {
            return this.moment(time).format('D MMMM, h:mm a');
          }
        }
      }
    }
    </script>
    
    <style scoped>
    .time {
      font-size: 10px;
      line-height: 10px;
    }
    </style>

    The getSetting method handles retrieving the appropriate setting based on the message type, and the formatTime method provides a nicely formatted timestamp.

  3. Create src/components/ChatLog.vue and add the following code.
  4. <template>
      <v-card-text>
        <v-card>
          <div
            v-chat-scroll="scrollSettings"
            class="d-flex flex-column pa-4 overflow-y-auto log"
            ref="log"
            :style="scrollHeight"
            @scroll="onScroll"
          >
            <chat-message
              v-for="message of messagesToDisplay"
              :key="message.id"
              :message="message"
            />
            <v-fab-transition>
              <v-btn
                v-if="showScrollButton"
                absolute
                class="scroll"
                color="primary"
                fab
                x-small
                @click="scrollToBottom()"
              >
                <v-icon>keyboard_arrow_down</v-icon>
              </v-btn>
            </v-fab-transition>
          </div>
          <v-expand-transition v-if="hasTypingStatus">
            <div class="px-4 py-1 primary white--text caption">
              {{ formattedTypingStatus }}
            </div>
          </v-expand-transition>
        </v-card>
      </v-card-text>
    </template>

    The ChatLog component handles the display of all chat messages. The v-chat-scroll property of the div element enables the scroll functionality provided by the VueChatScroll package we installed earlier. There are also two conditional elements that may be rendered—a button which will scroll back to the bottom of the chat log, and the notifications for when another user is typing a message.

    <script>
    import ChatMessage from './ChatMessage';
    
    export default {
      name: 'ChatLog',
      props: {
        messages: {
          type: Array,
          required: true
        },
        username: {
          type: String,
          required: true
        },
        typingStatus: {
          type: Object,
          required: true
        }
      },
      components: {
        ChatMessage
      },
      data: () => ({
        scrollSettings: {
          always: false,
          smooth: true
        },
        scrollDistance: 0,
        logHeight: 400,
        hasScrolled: false,
        showScrollButton: false
      }),

    This component takes in three props, an array of messages, the username of this user, and an object containing data on which other users are currently typing.

      computed: {
        hasTypingStatus() {
          return this.formattedTypingStatus !== '';
        },
        messagesToDisplay() {
          let lastSender = '';
          let messages = [];
    
          for (let message of this.messages) {
            message.type = this.getType(message);
            message.showSender =
              lastSender !== message.sender.uniqueName &&
              message.type === 'received';
            messages.push(message);
    
            lastSender = message.sender.uniqueName;
          }
    
          return messages;
        },
        scrollHeight() {
          return `max-height: ${this.logHeight}px;`;
        },
        formattedTypingStatus() {
          const users =
            Object.values(this.typingStatus).filter((status) => {
              return status.user.uniqueName !==
                this.username && status.typing
            }).map(status => status.user.uniqueName);
    
            if (users.length === 0) {
              return '';
            }
    
            const userString =
              users.join(', ').replace(/,([^,]*)$/, ' and $1');
            const userPlural = users.length > 1 ? 'are' : 'is';
    
            return `${userString} ${userPlural} typing...`
        }
      },

    There are several computed properties to help calculate variables used in the template. messagesToDisplay is used to build the array of messages that is rendered by the component. At the same time it also determines if the sender of the message is the first in a series, so that we know when to show the username on the messages.

      watch: {
        messages(value) {
          if (this.hasScrolled) {
            this.showScrollButton = true;
          }
        },
        scrollDistance(value) {
          if (value === 0) {
            this.hasScrolled = false;
            this.showScrollButton = false;
          }
        }
      },

    There are two properties being watched. When the value of messages changes we check to see if the user has scrolled away from the bottom, and if they have we show the button that allows them to scroll back to the bottom. The other watched property scrollDistance does the opposite, and checks to see when the scrollDistance is 0 (at the bottom) and then hides the scroll button again.

      methods: {
        getType(message) {
          return message.sender === 'Server' ?
            'server' :
            message.sender.uniqueName === this.username ?
              'sent' :
              'received';
        },
        onScroll(event) {
          this.hasScrolled = true;
          this.scrollDistance =
            event.target.scrollHeight -
            event.target.scrollTop -
            this.logHeight;
        },
        scrollToBottom() {
          this.$refs.log.scrollTop = this.$refs.log.scrollHeight;
        }
      }
    }
    </script>

    There are three methods in the ChatLog component, one to determine the type of the message, and two involving the scrolling behaviour. onScroll triggers when the user scrolls the view, and calculates how far the log has scrolled from the bottom.

    <style scoped>
    .log {
      scroll-behavior: smooth;
    }
    
    .scroll {
      right: 16px;
    }
    </style>

    Lastly, we’ll add a small amount of styling. The scroll-behavior css property will ensure the manual scrolling we added is smooth.

Send a Message

The messaging component of the app is fairly straightforward, with a simple v-textarea component.

  1. Create src/components/ChatSend.vue and add the following code:
  2. <template>
      <v-card-text>
        <v-textarea
          v-model="newMessage"
          clearable
          placeholder="Send chat message"
          rows="3"
          :autofocus="true"
          :no-resize="true"
          :solo="true"
          @keydown.enter="sendMessage()"
          @keydown.enter.prevent
          @keydown="textChanged()"
        ></v-textarea>
        <v-btn block color="primary" @click="sendMessage()">
          Send
        </v-btn>
      </v-card-text>
    </template>
    <script>
    export default {
      name: 'ChatSend',
      data: () => ({
        newMessage: ''
      }),
      methods: {
        sendMessage() {
          if (this.newMessage !== '') {
            this.$emit('send', this.newMessage);
            this.newMessage = '';
          }
        },
        textChanged() {
          this.$emit('is-typing');
        }
      }
    }
    </script>

    First we create the template with a text area component and button, then handle the send and is-typing message events.

List of Connected Users

Once the number of users increases a simple text list would become insufficient, so let’s use a v-chip to display each user and help keep things easier to read.

  1. Create src/components/ChatUserChip.vue and add the following code:
  2. <template>
      <v-chip
        class="mr-2 mb-2"
        :color="chipSettings[user.type].chipColour"
      >
        <v-avatar left>
          <v-icon :color="chipSettings[user.type].iconColour">
            {{ chipSettings[user.type].icon }}
          </v-icon>
        </v-avatar>
        {{ user.uniqueName }}
      </v-chip>
    </template>
    
    <script>
    export default {
      name: 'ChatUserChip',
      props: {
        user: {
          type: Object,
          required: true
        }
      },
      data: () => ({
        chipSettings: {
          currentUser: {
            chipColour: 'primary',
            icon: 'person',
            iconColour: 'white'
          },
          otherUser: {
            chipColour: 'default',
            icon: 'person',
            iconColour: 'error'
          }
        }
      })
    }
    </script>

    First, create the template—a chip with a username and icon with colour determined by chipSettings. The chip takes one prop, a user object which contains relevant data.

  3. Create src/components/ChatUsers.vue and add the following code:
  4. <template>
      <div>
        <v-card-subtitle>Connected Users</v-card-subtitle>
        <v-card-text>
          <chat-user-chip
            v-for="(user, i) of usersToDisplay"
            :key="i"
            :user="user"
          />
        </v-card-text>
      </div>
    </template>
    
    <script>
    import ChatUserChip from './ChatUserChip';
    
    export default {
      name: 'ChatUsers',
      props: {
        users: {
          type: Array,
          required: true
        },
        username: {
          type: String,
          required: true
        }
      },
      components: {
        ChatUserChip
      },
      computed: {
        usersToDisplay() {
          let users = [];
    
          for (let user of this.users) {
            user.type = this.username === user.uniqueName ?
              'currentUser' :
              'otherUser';
            users.push(user);
          }
    
          return users;
        }
      }
    }
    </script>

    The template will display a chip for each user in the usersToDisplay computed property. The same computed property will also determine if the user is the current user, or one of the other users, which will change the way the chip is rendered.

Bringing It All Together

Now we’ve built all of the components that we need to make the chat log work, let’s put the pieces together to create the final component.

  1. Create src/components/Chat.vue and add the following code.
  2. <template>
      <v-card class="elevation-0 grey lighten-4">
        <div v-if="!hasUser">
          <v-card-title>Connect to chat</v-card-title>
          <chat-login @set-username="setUsername" />
        </div>
        <div v-else class="elevation-1 px-4">
          <v-card-title>Connected as {{ user.uniqueName }}</v-card-title>
          <chat-log
            :messages="history"
            :typing-status="typingStatus"
            :username="user.uniqueName"
          />
          <chat-send @is-typing="setTypingStatus" @send="sendMessage" />
          <chat-users :users="users" :username="user.uniqueName" />
        </div>
      </v-card>
    </template>

    This template first checks to see if the user exists, and if not will display the ChatLogin component. If the user is logged in, we’ll display their username and then show the ChatLog, ChatSend, and ChatUsers components.

    <script>
    import { mapGetters } from 'vuex';
    import { addEventListener } from '../mixins/addEventListener';
    import ChatLog from './ChatLog';
    import ChatLogin from './ChatLogin';
    import ChatSend from './ChatSend';
    import ChatUsers from './ChatUsers';
    
    export default {
      name: 'Chat',
      mixins: [addEventListener],
      components: {
        ChatLog,
        ChatLogin,
        ChatSend,
        ChatUsers
      },
      data: () => ({
        user: undefined,
        typingStatus: {}
      }),

    The script section first imports the getters that we created in the vuex store, as well as the mixin we created in the last last part of this blog series, and the chat components we just created.

      computed: {
        ...mapGetters({
          CHAT_USERS: 'chat/GET_USERS',
          CHAT_HISTORY: 'chat/GET_HISTORY'
        }),
        users() {
          return this.CHAT_USERS ? this.CHAT_USERS : [];
        },
        history() {
          return this.CHAT_HISTORY ? this.CHAT_HISTORY : [];
        },
        hasUser() {
          return !!this.user;
        }
      },

    The computed ...mapGetters property uses spread syntax as a shorthand for retrieving all of our store data for local use. users and history ensure that missing data is handled appropriately, and provides a way to safely access data.

      mounted() {
        this.addEventListener('userList', (users) => {
          this.$store.dispatch('chat/RECEIVE_USERS', users);
        });
    
        this.addEventListener('userConnected', (user) => {
          this.$store.dispatch('chat/RECEIVE_USER', user);
        });
    
        this.addEventListener('userLogin', (user) => {
          this.user = user;
        });
    
        this.addEventListener('chatHistory', (history) => {
          this.$store.dispatch('chat/RECEIVE_HISTORY', history);
        });
    
        this.addEventListener('chatMessage', (message) => {
          this.$store.dispatch('chat/RECEIVE_MESSAGE', message);
        });
    
        this.addEventListener('typingStatus', (typingStatus) => {
          this.typingStatus = typingStatus;
        });
      },

    Here we add event listeners for the server events that we set up earlier. As you can see, some of them end up having their data sent to the store to update our data, and others are used locally.

      methods: {
        setUsername(username) {
          this.$store.dispatch('SEND_EVENT', {
            type: 'userConnected',
            data: username
          });
        },
        sendMessage(message) {
          this.$store.dispatch('SEND_EVENT', {
            type: 'chatMessage',
            data: {
              sender: this.user,
              text: message
            }
          });
        },
        setTypingStatus() {
          this.$store.dispatch('SEND_EVENT', {
            type: 'setTypingStatus',
            data: {
              user: this.user
            }
          });
        }
      }
    }
    </script>

    Lastly, add some methods to handle sending messages to the server.

  3. Update src/components/RealTimeDemo.vue with the following code.
  4. <template>
      <v-container>
        <v-row>
          <v-col :cols="4">
            <chat />
          </v-col>
          <v-col :cols="8">
          </v-col>
        </v-row>
      </v-container>
    </template>
    
    <script>
    import Chat from './Chat';
    
    export default {
      name: 'RealTimeDemo',
      components: {
        Chat
      }
    }
    </script>

    Finally, we can add our newly created Chat component to the RealTimeDemo component.

    We can now test our new functionality. Start the server with the command node server, and open the app in your browser. Enter a username and send some chat messages. Open the app multiple times in different browser tabs to see how it handles multiple connections.

    Stop the server by pressing the keys ctrl+c.

You can find this project in its entirety on GitHub.

Header image courtesy of NASA on Unsplash