Build a full-featured real-time chat application using Vue 3 and Firebase. We'll create a modern, responsive chat interface with real-time updates. 🚀
Project Setup 🎯
First, let's create a new Vue 3 project and install dependencies.
npm create vue@latest chat-app
cd chat-app
npm install firebase@latest pinia@latest vue-router@4
Firebase Configuration 🔥
// src/firebase/config.js
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
apiKey: 'your-api-key',
authDomain: 'your-auth-domain',
projectId: 'your-project-id',
storageBucket: 'your-storage-bucket',
messagingSenderId: 'your-messaging-sender-id',
appId: 'your-app-id'
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
export { db, auth };
Authentication Setup 🔒
// src/composables/useAuth.js
import { ref } from 'vue';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut as firebaseSignOut
} from 'firebase/auth';
import { auth } from '../firebase/config';
export function useAuth() {
const user = ref(null);
const error = ref(null);
async function signup(email, password) {
try {
const res = await createUserWithEmailAndPassword(
auth,
email,
password
);
user.value = res.user;
error.value = null;
} catch (err) {
error.value = err.message;
}
}
async function login(email, password) {
try {
const res = await signInWithEmailAndPassword(
auth,
email,
password
);
user.value = res.user;
error.value = null;
} catch (err) {
error.value = err.message;
}
}
async function signOut() {
try {
await firebaseSignOut(auth);
user.value = null;
} catch (err) {
error.value = err.message;
}
}
return { user, error, signup, login, signOut };
}
Chat Store with Pinia 📦
// src/stores/chat.js
import { defineStore } from 'pinia';
import {
collection,
addDoc,
query,
orderBy,
onSnapshot
} from 'firebase/firestore';
import { db } from '../firebase/config';
export const useChatStore = defineStore('chat', {
state: () => ({
messages: [],
loading: false,
error: null
}),
actions: {
async sendMessage(messageText, userId) {
try {
await addDoc(collection(db, 'messages'), {
text: messageText,
userId,
createdAt: new Date().toISOString()
});
} catch (error) {
this.error = error.message;
}
},
subscribeToMessages() {
const q = query(
collection(db, 'messages'),
orderBy('createdAt')
);
return onSnapshot(q, (snapshot) => {
this.messages = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
});
}
}
});
Chat Component 💬
<!-- src/components/ChatRoom.vue -->
<template>
<div class="chat-container">
<div class="messages" ref="messagesContainer">
<div
v-for="message in messages"
:key="message.id"
:class="['message', {
'message--own': message.userId === user?.uid
}]"
>
<div class="message__content">
{{ message.text }}
</div>
<div class="message__time">
{{ formatTime(message.createdAt) }}
</div>
</div>
</div>
<form @submit.prevent="sendMessage" class="message-form">
<input
v-model="newMessage"
type="text"
placeholder="Type a message..."
:disabled="!user"
/>
<button
type="submit"
:disabled="!newMessage.trim() || !user"
>
Send
</button>
</form>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useChatStore } from '../stores/chat';
import { useAuth } from '../composables/useAuth';
import { formatTime } from '../utils/datetime';
const chatStore = useChatStore();
const { messages } = storeToRefs(chatStore);
const { user } = useAuth();
const newMessage = ref('');
const messagesContainer = ref(null);
let unsubscribe = null;
onMounted(() => {
unsubscribe = chatStore.subscribeToMessages();
});
onUnmounted(() => {
if (unsubscribe) {
unsubscribe();
}
});
watch(messages, () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop =
messagesContainer.value.scrollHeight;
}
});
});
async function sendMessage() {
if (!newMessage.value.trim() || !user.value) return;
await chatStore.sendMessage(
newMessage.value,
user.value.uid
);
newMessage.value = '';
}
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 70%;
padding: 0.5rem 1rem;
border-radius: 1rem;
background: #f1f1f1;
}
.message--own {
align-self: flex-end;
background: #0084ff;
color: white;
}
.message__content {
margin-bottom: 0.25rem;
}
.message__time {
font-size: 0.75rem;
opacity: 0.7;
}
.message-form {
display: flex;
gap: 1rem;
padding: 1rem;
background: white;
border-top: 1px solid #eee;
}
input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 0.5rem;
}
button {
padding: 0.5rem 1rem;
background: #0084ff;
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
Router Setup 🛣️
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { auth } from '../firebase/config';
const routes = [
{
path: '/',
name: 'Chat',
component: () => import('../views/ChatView.vue'),
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: () => import('../views/LoginView.vue')
},
{
path: '/signup',
name: 'Signup',
component: () => import('../views/SignupView.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to, from, next) => {
const requiresAuth = to.matched.some(
record => record.meta.requiresAuth
);
if (requiresAuth && !auth.currentUser) {
next('/login');
} else {
next();
}
});
export default router;
User Interface Components 🎨
Login Form
<!-- src/components/LoginForm.vue -->
<template>
<form @submit.prevent="handleSubmit" class="auth-form">
<h2>Login</h2>
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
v-model="email"
type="email"
required
/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
id="password"
v-model="password"
type="password"
required
/>
</div>
<div v-if="error" class="error">
{{ error }}
</div>
<button type="submit" :disabled="loading">
{{ loading ? 'Loading...' : 'Login' }}
</button>
</form>
</template>
<script setup>
import { ref } from 'vue';
import { useAuth } from '../composables/useAuth';
import { useRouter } from 'vue-router';
const email = ref('');
const password = ref('');
const loading = ref(false);
const { login, error } = useAuth();
const router = useRouter();
async function handleSubmit() {
loading.value = true;
await login(email.value, password.value);
if (!error.value) {
router.push('/');
}
loading.value = false;
}
</script>
Real-Time Features 🔄
Online Status
// src/composables/useOnlineStatus.js
import { ref, onMounted, onUnmounted } from 'vue';
import { db, auth } from '../firebase/config';
import {
doc,
setDoc,
serverTimestamp,
onDisconnect
} from 'firebase/firestore';
export function useOnlineStatus() {
const isOnline = ref(true);
onMounted(() => {
if (auth.currentUser) {
const userStatusRef = doc(
db,
'status',
auth.currentUser.uid
);
setDoc(userStatusRef, {
state: 'online',
lastChanged: serverTimestamp()
});
onDisconnect(userStatusRef).set({
state: 'offline',
lastChanged: serverTimestamp()
});
}
});
return { isOnline };
}
Deployment 🚀
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
target: 'esnext',
minify: 'terser'
}
});
Additional Features 🌟
- Typing indicators
- Read receipts
- File sharing
- User profiles
- Group chats
Best Practices 📝
- Use Composition API for better code organization
- Implement proper error handling
- Add loading states
- Optimize performance
- Secure Firebase rules
- Add proper validation
- Handle offline support
- Implement proper testing
- Use proper TypeScript types
- Follow Vue style guide
Additional Resources
Building a real-time chat application with Vue 3 and Firebase provides a great foundation for understanding modern web development concepts. This implementation can be extended with additional features based on your specific needs.