Compared to all the other things you can add to your Next.Js projects, chat functionality has to be one of the easiest things you can implement. Why? Because there are a bunch of solutions out there for good reasons. A lot of websites are popping up with live chat features, more often than not for help, support, and to talk to a representative, which is why it’s important to know how to code a chat feature.
But, of course, you already know that, right? Why else would you click on this tutorial walking you through how to do it? And you’re probably gonna skip this introduction anyway to get into the nitty-gritty. So, let’s get into coder mode and get started!
Getting Started
Learn all about Firebase on the website. You might fall in love with it. And if you don’t know what Next.Js is…seriously, what are you doing here? But you can learn more about this great React framework in its documentation.
To follow this tutorial, create a Firestore collection called ‘users’ with a name field. Then, create a collection called ‘chats’ with the fields being a timestamp called ‘createdAt’ and an array called ‘participants’. Inside the document, create a collection called ‘messages’ with fields ‘senderId’, ‘text’, and ‘timestamp’.
You also need to ensure you have a firebase.js
file in the app directory of your project. Inside should be your Firebase config:
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";
import { getStorage } from 'firebase/storage';
const firebaseConfig = {
apiKey: "API_KEY",
authDomain: "AUTH_DOMIAN",
projectId: "PROJECT_ID",
storageBucket: "STORAGE_BUCKET",
messagingSenderId: "MESSAGE_SENDER_ID",
appId: "APP_ID"
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);
const storage = getStorage(app);
export { db, auth, storage };
Before we can start on the chat component, we can make life easier by creating a users
file with the following code:
import { collection, getDocs } from 'firebase/firestore';
import { db } from '@/app/firebase';
interface User {
id: string;
name: string;
}
async function fetchUsers(): Promise<User[]> {
const usersCollection = collection(db, 'users');
const usersSnapshot = await getDocs(usersCollection);
const usersList = usersSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as User));
return usersList;
}
export {
fetchUsers
};
With that setup, let’s get started with the actual chat code.
Breaking Down The Chat Code
Now, we can create a file in the folder with all the rest of our components (this folder may vary depending on your setup or you might need to create a components
folder in your app directory) and let’s name the file chat.jsx
.
These are the self-explanatory imports you should have:
'use client';
import { useEffect, useState, useRef } from 'react';
import { getFirestore, collection, addDoc, Timestamp, query, onSnapshot, orderBy, getDocs } from 'firebase/firestore';
import { useRouter, useSearchParams } from 'next/navigation';
import { fetchUsers } from './users';
We also need to declare quite a bit:
const searchParams = useSearchParams();
const router = useRouter();
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState('');
const chatContainerRef = useRef(null); // Ref for chat container
const user1Id = searchParams.get('user1Id'); // Get user1Id from URL parameters
const user2Id = searchParams.get('user2Id'); // Get user2Id from URL parameters
const [user1, setUser1] = useState(null);
const [user2, setUser2] = useState(null);
const [isReversed, setIsReversed] = useState(false);
const [currentUserId, setCurrentUserId] = useState(null);
const [chatId, setChatId] = useState(null);
const db = getFirestore();
We also need to set the current user id:
useEffect(() => {
const id = searchParams.get('user1Id');
setCurrentUserId(id);
}, []);
Next, let’s create a chat session:
const createChatSession = async (participants) => {
try {
const chatDocRef = await addDoc(collection(db, 'chats'), {
participants,
createdAt: Timestamp.fromDate(new Date()),
});
return chatDocRef.id;
} catch (error) {
console.error("Error creating chat session:", error);
throw error;
}
};
We also need to fetch all of the necessary data, such as the user’s info, and retrieve the chat session if there is one:
useEffect(() => {
const fetchData = async () => {
const users = await fetchUsers();
const user1Data = users.find(user => user.id === user1Id);
const user2Data = users.find(user => user.id === user2Id);
setUser1(user1Data || null);
setUser2(user2Data || null);
if (currentUserId === user1Id) {
setIsReversed(false);
} else if (currentUserId === user2Id) {
setIsReversed(true);
}
const chatDocRef = collection(db, 'chats');
const chatQuery = query(chatDocRef, orderBy('createdAt'));
const chatSnapshot = await getDocs(chatQuery);
const chatDoc = chatSnapshot.docs.find(doc => doc.data().participants.includes(user1Id) && doc.data().participants.includes(user2Id));
if (chatDoc) {
setChatId(chatDoc.id);
} else {
const newChatId = await createChatSession([user1Id, user2Id]);
setChatId(newChatId);
}
};
fetchData();
}, [user1Id, user2Id, currentUserId, db]);
We also will get the messages:
useEffect(() => {
if (!chatId) return;
const q = query(collection(db, `chats/${chatId}/messages`), orderBy('timestamp'));
const unsubscribe = onSnapshot(q, (snapshot) => {
const newMessages = snapshot.docs.map(doc => doc.data());
setMessages(newMessages);
});
console.log('Messages')
return () => unsubscribe();
}, [chatId, db]);
After we get the messages, we need to ensure that the messages appear on the bottom first, and a part of doing that will be thanks to adding this code:
useEffect(() => {
// Scroll to the bottom of the chat container when new messages are added
if (chatContainerRef.current) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}
}, [messages]);
Handle sending messages:
const handleSendMessage = async (messageText) => {
if (messageText.trim() && chatId) {
await addDoc(collection(db, `chats/${chatId}/messages`), {
senderId: currentUserId,
text: messageText,
timestamp: Timestamp.fromDate(new Date()),
});
setNewMessage('');
}
};
Then let’s generate the chat link so the other user can join:
const generateChatLink = () => {
const currentUrl = window.location.origin;
const chatUrl = `${currentUrl}?user1Id=${user2.id}&user2Id=${user1.id}&chatId=${chatId}`;
return chatUrl;
};
const chatLink = generateChatLink();
Display the link:
<div className='p-4'>
<p>Share this link with {user2.name} to join the chat:</p>
<input type="text" value={chatLink} className="text-black" readOnly style={{ width: '100%', padding: '8px', marginBottom: '10px' }} />
<button onClick={() => navigator.clipboard.writeText(chatLink)} className='bg-blue-500 text-white p-2 rounded'>Copy Link</button>
</div>
Add the container for the messages:
<div
className="chat-container bg-opacity-55 bg-slate-900 m-4 p-4"
ref={chatContainerRef}
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
overflowY: 'auto',
height: 'calc(80vh - 220px)',
marginTop: '10px',
border: '1px solid red',
}}
>
{messages.map((message, index) => (
<div
key={index}
className={`message ${message.senderId === user1.id ? (isReversed ? 'received' : 'sent') : (isReversed ? 'sent' : 'received')}`}
style={{
backgroundColor: message.senderId === user1.id
? (isReversed ? 'lightgray' : 'blue')
: (isReversed ? 'blue' : 'lightgray'),
color: message.senderId === user1.id ? 'white' : 'black',
borderRadius: '10px',
padding: '10px',
marginBottom: '10px',
alignSelf: message.senderId === user1.id ? 'flex-end' : 'flex-start',
maxWidth: '60%',
}}
>
<div className="bubble text-black">{message.text}</div>
</div>
))}
</div>
Lastly, add a way to type the message:
const MessageInput = ({ onSendMessage }) => {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
setInputValue(event.target.value);
};
const handleSend = () => {
if (inputValue.trim() !== '') {
onSendMessage(inputValue);
setInputValue('');
}
};
return (
<div className="message-input" style={{ position: 'absolute', bottom: 0, width: '100%', display: 'flex', alignItems: 'center', padding: '10px', backgroundColor: 'rgba(0, 0, 0, 0.5)' }}>
<input
type="text"
value={inputValue}
onChange={handleChange}
style={{ flex: 1, marginRight: '10px', padding: '10px', borderRadius: '5px', border: '1px solid #fff', color: '#000' }}
/>
<button
onClick={handleSend}
style={{ padding: '10px', borderRadius: '5px', backgroundColor: '#007BFF', color: '#fff', border: 'none' }}>
Send
</button>
</div>
);
};
Now, let’s put the pieces together!
The Full Code
This is the full code:
'use client';
import { useEffect, useState, useRef } from 'react';
import { getFirestore, collection, addDoc, Timestamp, query, onSnapshot, orderBy, getDocs } from 'firebase/firestore';
import { useRouter, useSearchParams } from 'next/navigation';
import { fetchUsers } from './users';
const ChatPage = () => {
const searchParams = useSearchParams();
const router = useRouter();
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState('');
const chatContainerRef = useRef(null); // Ref for chat container
const user1Id = searchParams.get('user1Id'); // Get user1Id from URL parameters
const user2Id = searchParams.get('user2Id'); // Get user2Id from URL parameters
const [user1, setUser1] = useState(null);
const [user2, setUser2] = useState(null);
const [isReversed, setIsReversed] = useState(false);
const [currentUserId, setCurrentUserId] = useState(null);
const [chatId, setChatId] = useState(null);
const db = getFirestore();
useEffect(() => {
// Get the user ID from local storage
const id = searchParams.get('user1Id');
setCurrentUserId(id);
}, []);
const createChatSession = async (participants) => {
try {
// Add a new chat document to the 'chats' collection
const chatDocRef = await addDoc(collection(db, 'chats'), {
participants,
createdAt: Timestamp.fromDate(new Date()), // Add creation timestamp
});
return chatDocRef.id; // Return the new chat document ID
} catch (error) {
console.error("Error creating chat session:", error);
throw error;
}
};
useEffect(() => {
const fetchData = async () => {
const users = await fetchUsers(); // Fetch all users
const user1Data = users.find(user => user.id === user1Id);
const user2Data = users.find(user => user.id === user2Id);
setUser1(user1Data || null);
setUser2(user2Data || null);
// Determine user roles
if (currentUserId === user1Id) {
setIsReversed(false); // Normal color scheme for user1
} else if (currentUserId === user2Id) {
setIsReversed(true); // Reverse colors for user2
}
// Create or retrieve chat session
const chatDocRef = collection(db, 'chats');
const chatQuery = query(chatDocRef, orderBy('createdAt'));
const chatSnapshot = await getDocs(chatQuery);
const chatDoc = chatSnapshot.docs.find(doc => doc.data().participants.includes(user1Id) && doc.data().participants.includes(user2Id));
if (chatDoc) {
setChatId(chatDoc.id);
} else {
const newChatId = await createChatSession([user1Id, user2Id]);
setChatId(newChatId);
}
};
fetchData();
}, [user1Id, user2Id, currentUserId, db]);
useEffect(() => {
if (!chatId) return;
const q = query(collection(db, `chats/${chatId}/messages`), orderBy('timestamp'));
const unsubscribe = onSnapshot(q, (snapshot) => {
const newMessages = snapshot.docs.map(doc => doc.data());
setMessages(newMessages);
});
console.log('Messages')
return () => unsubscribe();
}, [chatId, db]);
useEffect(() => {
// Scroll to the bottom of the chat container when new messages are added
if (chatContainerRef.current) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}
}, [messages]);
const handleSendMessage = async (messageText) => {
if (messageText.trim() && chatId) {
await addDoc(collection(db, `chats/${chatId}/messages`), {
senderId: currentUserId,
text: messageText,
timestamp: Timestamp.fromDate(new Date()),
});
setNewMessage(''); // Clear the input field
}
};
if (!user1 || !user2 || !chatId) {
return <div>Loading...</div>;
}
const generateChatLink = () => {
const currentUrl = window.location.origin;
const chatUrl = `${currentUrl}?user1Id=${user2.id}&user2Id=${user1.id}&chatId=${chatId}`;
return chatUrl;
};
const chatLink = generateChatLink();
return (
<div>
<div style={{ position: 'relative', zIndex: 1, height: '100vh' }}>
<h2 className='text-purple-300 text-xl'>Chat with {user2.name}</h2>
<div className='p-4'>
<p>Share this link with {user2.name} to join the chat:</p>
<input type="text" value={chatLink} className="text-black" readOnly style={{ width: '100%', padding: '8px', marginBottom: '10px' }} />
<button onClick={() => navigator.clipboard.writeText(chatLink)} className='bg-blue-500 text-white p-2 rounded'>Copy Link</button>
</div>
<div
className="chat-container bg-opacity-55 bg-slate-900 m-4 p-4"
ref={chatContainerRef}
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
overflowY: 'auto',
height: 'calc(80vh - 220px)',
marginTop: '10px',
border: '1px solid red',
}}
>
{messages.map((message, index) => (
<div
key={index}
className={`message ${message.senderId === user1.id ? (isReversed ? 'received' : 'sent') : (isReversed ? 'sent' : 'received')}`}
style={{
backgroundColor: message.senderId === user1.id
? (isReversed ? 'lightgray' : 'blue')
: (isReversed ? 'blue' : 'lightgray'),
color: message.senderId === user1.id ? 'white' : 'black',
borderRadius: '10px',
padding: '10px',
marginBottom: '10px',
alignSelf: message.senderId === user1.id ? 'flex-end' : 'flex-start',
maxWidth: '60%', // Ensure messages do not stretch too far horizontally
}}
>
<div className="bubble text-black">{message.text}</div>
</div>
))}
</div>
<MessageInput onSendMessage={handleSendMessage} />
</div>
</div>
);
};
const MessageInput = ({ onSendMessage }) => {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
setInputValue(event.target.value);
};
const handleSend = () => {
if (inputValue.trim() !== '') {
onSendMessage(inputValue); // Pass the inputValue to the onSendMessage function
setInputValue('');
}
};
return (
<div className="message-input" style={{ position: 'absolute', bottom: 0, width: '100%', display: 'flex', alignItems: 'center', padding: '10px', backgroundColor: 'rgba(0, 0, 0, 0.5)' }}>
<input
type="text"
value={inputValue}
onChange={handleChange}
style={{ flex: 1, marginRight: '10px', padding: '10px', borderRadius: '5px', border: '1px solid #fff', color: '#000' }}
/>
<button
onClick={handleSend}
style={{ padding: '10px', borderRadius: '5px', backgroundColor: '#007BFF', color: '#fff', border: 'none' }}>
Send
</button>
</div>
);
};
export default ChatPage;
Modify The Code
The code I provided is not a one-size-fits-all solution. It’s more like a starting point to push you in the right direction.
You just learned how to code a basic chat page with Firebase and Next.js. That’s one simple thing you can cross off your to-do list. Well, that’s about it for this article.
Make sure you follow me on Medium and subscribe to my newsletter.
Happy Coding Folks!
Top comments (0)