How to use Vue with WebSockets

Implementing WebSockets in Vue enables real-time, bidirectional communication for live updates, chat applications, and streaming data. As the creator of CoreUI with over 12 years of Vue.js experience since 2014, I’ve built real-time dashboards with WebSocket connections. Vue’s reactivity system works seamlessly with WebSocket messages, automatically updating the UI when new data arrives. This approach creates responsive applications with live data synchronization without polling.

Use native WebSocket API with Vue’s reactivity to implement real-time communication and live data updates.

Basic WebSocket setup:

<template>
  <div>
    <div>Status: {{ connectionStatus }}</div>
    <div>
      <h3>Messages</h3>
      <ul>
        <li v-for='(msg, index) in messages' :key='index'>
          {{ msg.text }} - {{ msg.time }}
        </li>
      </ul>
    </div>
    <div>
      <input v-model='newMessage' @keyup.enter='sendMessage' placeholder='Type message'>
      <button @click='sendMessage'>Send</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const ws = ref(null)
const connectionStatus = ref('Disconnected')
const messages = ref([])
const newMessage = ref('')

const connectWebSocket = () => {
  ws.value = new WebSocket('ws://localhost:8080')

  ws.value.onopen = () => {
    connectionStatus.value = 'Connected'
    console.log('WebSocket connected')
  }

  ws.value.onmessage = (event) => {
    const data = JSON.parse(event.data)
    messages.value.push({
      text: data.message,
      time: new Date().toLocaleTimeString()
    })
  }

  ws.value.onerror = (error) => {
    console.error('WebSocket error:', error)
    connectionStatus.value = 'Error'
  }

  ws.value.onclose = () => {
    connectionStatus.value = 'Disconnected'
    console.log('WebSocket disconnected')

    // Reconnect after 3 seconds
    setTimeout(connectWebSocket, 3000)
  }
}

const sendMessage = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN && newMessage.value) {
    ws.value.send(JSON.stringify({
      type: 'message',
      message: newMessage.value
    }))
    newMessage.value = ''
  }
}

onMounted(() => {
  connectWebSocket()
})

onUnmounted(() => {
  if (ws.value) {
    ws.value.close()
  }
})
</script>

Composable for WebSocket management:

// useWebSocket.js
import { ref, onUnmounted } from 'vue'

export function useWebSocket(url) {
  const ws = ref(null)
  const isConnected = ref(false)
  const messages = ref([])

  const connect = () => {
    ws.value = new WebSocket(url)

    ws.value.onopen = () => {
      isConnected.value = true
    }

    ws.value.onmessage = (event) => {
      const data = JSON.parse(event.data)
      messages.value.push(data)
    }

    ws.value.onerror = (error) => {
      console.error('WebSocket error:', error)
    }

    ws.value.onclose = () => {
      isConnected.value = false
      setTimeout(connect, 3000)
    }
  }

  const send = (data) => {
    if (ws.value && ws.value.readyState === WebSocket.OPEN) {
      ws.value.send(JSON.stringify(data))
    }
  }

  const close = () => {
    if (ws.value) {
      ws.value.close()
    }
  }

  onUnmounted(() => {
    close()
  })

  return {
    isConnected,
    messages,
    connect,
    send,
    close
  }
}

Using the composable:

<template>
  <div>
    <div v-if='!isConnected'>Connecting...</div>
    <div v-else>
      <h3>Real-time Data</h3>
      <ul>
        <li v-for='msg in messages' :key='msg.id'>
          {{ msg.content }}
        </li>
      </ul>
      <button @click='send({ type: "ping" })'>Send Ping</button>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { useWebSocket } from './useWebSocket'

const { isConnected, messages, connect, send } = useWebSocket('ws://localhost:8080')

onMounted(() => {
  connect()
})
</script>

Live dashboard with WebSocket:

<template>
  <div class='dashboard'>
    <div class='status-bar'>
      <span :class='statusClass'>{{ status }}</span>
      <span>Users online: {{ onlineUsers }}</span>
    </div>

    <div class='stats'>
      <div class='stat-card' v-for='stat in stats' :key='stat.name'>
        <h4>{{ stat.name }}</h4>
        <p class='value'>{{ stat.value }}</p>
      </div>
    </div>

    <div class='activity-feed'>
      <h3>Live Activity</h3>
      <div v-for='activity in recentActivity' :key='activity.id' class='activity-item'>
        <span class='time'>{{ formatTime(activity.timestamp) }}</span>
        <span>{{ activity.message }}</span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const ws = ref(null)
const status = ref('Connecting')
const onlineUsers = ref(0)
const stats = ref([
  { name: 'Sales', value: 0 },
  { name: 'Orders', value: 0 },
  { name: 'Revenue', value: 0 }
])
const recentActivity = ref([])

const statusClass = computed(() => ({
  'status-connected': status.value === 'Connected',
  'status-disconnected': status.value === 'Disconnected',
  'status-error': status.value === 'Error'
}))

const connectWebSocket = () => {
  ws.value = new WebSocket('ws://localhost:8080/dashboard')

  ws.value.onopen = () => {
    status.value = 'Connected'
  }

  ws.value.onmessage = (event) => {
    const data = JSON.parse(event.data)

    switch (data.type) {
      case 'stats':
        stats.value = data.stats
        break
      case 'users':
        onlineUsers.value = data.count
        break
      case 'activity':
        recentActivity.value.unshift(data.activity)
        if (recentActivity.value.length > 20) {
          recentActivity.value.pop()
        }
        break
    }
  }

  ws.value.onerror = () => {
    status.value = 'Error'
  }

  ws.value.onclose = () => {
    status.value = 'Disconnected'
    setTimeout(connectWebSocket, 3000)
  }
}

const formatTime = (timestamp) => {
  return new Date(timestamp).toLocaleTimeString()
}

onMounted(() => {
  connectWebSocket()
})

onUnmounted(() => {
  if (ws.value) {
    ws.value.close()
  }
})
</script>

<style scoped>
.dashboard {
  padding: 20px;
}

.status-bar {
  display: flex;
  justify-content: space-between;
  padding: 10px;
  background: #f5f5f5;
  margin-bottom: 20px;
}

.status-connected {
  color: #28a745;
  font-weight: bold;
}

.status-disconnected {
  color: #dc3545;
}

.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.stat-card {
  padding: 20px;
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.value {
  font-size: 32px;
  font-weight: bold;
  margin: 0;
}

.activity-feed {
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 20px;
}

.activity-item {
  padding: 10px 0;
  border-bottom: 1px solid #eee;
}

.time {
  color: #666;
  margin-right: 10px;
}
</style>

Chat application:

<template>
  <div class='chat-container'>
    <div class='messages'>
      <div
        v-for='msg in messages'
        :key='msg.id'
        :class="['message', msg.sender === currentUser ? 'own-message' : 'other-message']"
      >
        <strong>{{ msg.sender }}:</strong> {{ msg.text }}
        <span class='timestamp'>{{ msg.time }}</span>
      </div>
    </div>
    <div class='input-area'>
      <input
        v-model='messageInput'
        @keyup.enter='sendMessage'
        placeholder='Type a message...'
      >
      <button @click='sendMessage'>Send</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'

const ws = ref(null)
const messages = ref([])
const messageInput = ref('')
const currentUser = ref('User' + Math.floor(Math.random() * 1000))

const connectChat = () => {
  ws.value = new WebSocket('ws://localhost:8080/chat')

  ws.value.onopen = () => {
    ws.value.send(JSON.stringify({
      type: 'join',
      user: currentUser.value
    }))
  }

  ws.value.onmessage = (event) => {
    const data = JSON.parse(event.data)
    messages.value.push({
      id: Date.now(),
      sender: data.user,
      text: data.message,
      time: new Date().toLocaleTimeString()
    })

    nextTick(() => {
      scrollToBottom()
    })
  }
}

const sendMessage = () => {
  if (messageInput.value.trim() && ws.value) {
    ws.value.send(JSON.stringify({
      type: 'message',
      user: currentUser.value,
      message: messageInput.value
    }))
    messageInput.value = ''
  }
}

const scrollToBottom = () => {
  const messagesEl = document.querySelector('.messages')
  if (messagesEl) {
    messagesEl.scrollTop = messagesEl.scrollHeight
  }
}

onMounted(() => {
  connectChat()
})

onUnmounted(() => {
  if (ws.value) {
    ws.value.close()
  }
})
</script>

<style scoped>
.chat-container {
  max-width: 600px;
  margin: 0 auto;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.messages {
  height: 400px;
  overflow-y: auto;
  padding: 20px;
  background: #f9f9f9;
}

.message {
  margin: 10px 0;
  padding: 10px;
  border-radius: 8px;
  max-width: 70%;
}

.own-message {
  background: #007bff;
  color: white;
  margin-left: auto;
}

.other-message {
  background: white;
}

.timestamp {
  font-size: 12px;
  opacity: 0.7;
  margin-left: 10px;
}

.input-area {
  display: flex;
  padding: 10px;
  background: white;
  border-top: 1px solid #ddd;
}

.input-area input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-right: 10px;
}

.input-area button {
  padding: 10px 20px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

Best Practice Note

Always implement reconnection logic for WebSocket disconnections—network issues are common. Use JSON.stringify and JSON.parse for structured message passing. Check readyState before sending to avoid errors. Clean up WebSocket connections in onUnmounted to prevent memory leaks. Use composables to encapsulate WebSocket logic for reusability. Consider heartbeat/ping messages to keep connections alive. This is how we implement WebSockets in CoreUI for Vue—providing real-time dashboards, live notifications, and chat features with automatic reconnection and clean state management for production applications.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team