DEV Community

It's Just Nifty
It's Just Nifty

Posted on • Originally published at niftylittleme.com on

Adding Chat Functionality To Your Next.Js Project With Firebase

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!

Original Image

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 };
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

We also need to set the current user id:

  useEffect(() => {
    const id = searchParams.get('user1Id');
    setCurrentUserId(id);
  }, []);
Enter fullscreen mode Exit fullscreen mode

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;
    }
  };
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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('');
    }
  };
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)