Building a Real-Time Chat App with React and Firebase

In this tutorial, we will build a real-time chat application using React and Firebase. Real-time chat apps allow users to communicate with each other instantly, with messages being delivered and displayed in real-time. React is a popular JavaScript library for building user interfaces, while Firebase is a platform that provides various backend services, including real-time database functionality. By combining React and Firebase, we can create a powerful and efficient chat app that updates in real-time.

building real time chat app react firebase

Introduction

A real-time chat app is an application that allows users to send and receive messages in real-time. Unlike traditional messaging apps, real-time chat apps update the chat interface as soon as new messages are sent, without requiring the user to refresh the page. This provides a more seamless and interactive user experience.

React is a JavaScript library for building user interfaces. It allows developers to create reusable UI components that update efficiently and in a declarative manner. Firebase, on the other hand, is a platform that provides various backend services, including a real-time database. It offers a simple and scalable way to store and sync data in real-time.

In this tutorial, we will use React to build the chat interface and Firebase to handle the real-time updates and data storage. We will start by setting up the project and initializing Firebase. Then, we will build the chat interface, including the ability to display and send messages. We will also add user authentication to secure the chat app. Finally, we will explore additional features such as typing indicators, user presence status, and message reactions. We will conclude by deploying the app to Firebase Hosting.

Setting up the project

Before we can start building the chat app, we need to set up the project and install the necessary dependencies.

Creating a new React project

To create a new React project, we can use the create-react-app command-line tool. Open your terminal and run the following command:

npx create-react-app chat-app

This will create a new directory named chat-app and set up a basic React project inside it.

Installing Firebase

Next, we need to install the Firebase JavaScript SDK. Open your terminal and navigate to the project directory (chat-app) and run the following command:

npm install firebase

This will install the Firebase JavaScript SDK and its dependencies in our project.

Initializing Firebase in the project

To initialize Firebase in our project, we need to create a Firebase project and obtain the configuration settings.

  1. Go to the Firebase Console and create a new project.
  2. Once the project is created, click on the "Add Firebase to your web app" button. This will display the configuration settings.
  3. Copy the configuration settings and open the src/firebase.js file in your project.
  4. Replace the existing content of the file with the following code:
import firebase from 'firebase/app';
import 'firebase/firestore';

const firebaseConfig = {
  // Paste your Firebase configuration settings here
};

firebase.initializeApp(firebaseConfig);

export default firebase;

Replace the comment // Paste your Firebase configuration settings here with the configuration settings obtained from the Firebase console.

With this setup, we have successfully installed and initialized Firebase in our React project.

Building the chat interface

Now that we have set up the project and initialized Firebase, we can start building the chat interface.

Creating the chat component

To create the chat component, we will create a new file named Chat.js in the src directory of our project. Open the file and add the following code:

import React, { useEffect, useState } from 'react';
import firebase from './firebase';

const Chat = () => {
  const [messages, setMessages] = useState([]);
  const [newMessage, setNewMessage] = useState('');

  useEffect(() => {
    const unsubscribe = firebase
      .firestore()
      .collection('messages')
      .orderBy('timestamp')
      .onSnapshot(snapshot => {
        const data = snapshot.docs.map(doc => doc.data());
        setMessages(data);
      });

    return () => unsubscribe();
  }, []);

  const sendMessage = async () => {
    if (newMessage.trim() === '') return;

    await firebase.firestore().collection('messages').add({
      text: newMessage,
      timestamp: firebase.firestore.FieldValue.serverTimestamp(),
    });

    setNewMessage('');
  };

  return (
    <div>
      <h1>Chat</h1>
      <div>
        {messages.map(message => (
          <div key={message.id}>
            <p>{message.text}</p>
          </div>
        ))}
      </div>
      <input
        type="text"
        value={newMessage}
        onChange={e => setNewMessage(e.target.value)}
      />
      <button onClick={sendMessage}>Send</button>
    </div>
  );
};

export default Chat;

In this code, we import the necessary dependencies and initialize the state variables messages and newMessage using the useState hook. The messages state variable will store an array of messages, while the newMessage state variable will store the text of the new message being composed.

We use the useEffect hook to listen for real-time updates from the Firebase Firestore database. The onSnapshot method allows us to listen for changes in the messages collection and update the messages state variable accordingly. We also use the orderBy method to sort the messages by their timestamp field.

The sendMessage function is called when the "Send" button is clicked. It checks if the newMessage is not empty and then adds a new document to the messages collection in the Firestore database. We use the serverTimestamp method to set the timestamp field to the current server timestamp.

In the return statement, we render the chat interface. We map over the messages array and render each message as a <p> element. We also render an input field and a button for composing and sending new messages.

Displaying messages

To display the messages in the chat interface, we map over the messages array in the return statement of the Chat component and render each message as a <p> element. The key prop is set to the id of each message to ensure efficient updates.

{messages.map(message => (
  <div key={message.id}>
    <p>{message.text}</p>
  </div>
))}

Sending messages

To send a new message, we add an input field and a button to the chat interface. The value of the input field is controlled by the newMessage state variable, and the onChange event updates the newMessage state variable as the user types.

<input
  type="text"
  value={newMessage}
  onChange={e => setNewMessage(e.target.value)}
/>
<button onClick={sendMessage}>Send</button>

The sendMessage function is called when the "Send" button is clicked. It checks if the newMessage is not empty and then adds a new document to the messages collection in the Firestore database.

const sendMessage = async () => {
  if (newMessage.trim() === '') return;

  await firebase.firestore().collection('messages').add({
    text: newMessage,
    timestamp: firebase.firestore.FieldValue.serverTimestamp(),
  });

  setNewMessage('');
};

Adding user authentication

To secure the chat app and allow users to authenticate, we can use Firebase Authentication. Follow the Firebase documentation to set up user authentication using Firebase Authentication.

Once user authentication is set up, we can update the sendMessage function to include the user's ID and display name in the message document.

const user = firebase.auth().currentUser;

await firebase.firestore().collection('messages').add({
  text: newMessage,
  timestamp: firebase.firestore.FieldValue.serverTimestamp(),
  userId: user.uid,
  displayName: user.displayName,
});

With this update, the messages collection will include the userId and displayName fields for each message, allowing us to identify the sender and display their name in the chat interface.

Real-time updates with Firebase

Firebase provides real-time updates out of the box, allowing us to listen for changes in the database and update the chat interface in real-time.

Listening for new messages

To listen for new messages in real-time, we use the onSnapshot method of the Firestore collection. In the useEffect hook, we set up a listener that updates the messages state variable whenever a new message is added to the messages collection.

useEffect(() => {
  const unsubscribe = firebase
    .firestore()
    .collection('messages')
    .orderBy('timestamp')
    .onSnapshot(snapshot => {
      const data = snapshot.docs.map(doc => doc.data());
      setMessages(data);
    });

  return () => unsubscribe();
}, []);

The orderBy method is used to sort the messages by their timestamp field, ensuring that they are displayed in the correct order.

Updating the chat interface in real-time

With the listener set up, the chat interface will update in real-time as new messages are added to the Firestore database. The messages state variable is updated with the latest messages, and the interface is re-rendered to reflect the changes.

{messages.map(message => (
  <div key={message.id}>
    <p>{message.text}</p>
  </div>
))}

As new messages are added, they will automatically appear in the chat interface without requiring the user to refresh the page.

Adding additional features

Now that we have a basic chat app with real-time updates, we can add additional features to enhance the user experience.

Typing indicators

Typing indicators can be added to show when a user is typing a message. We can use Firebase Cloud Firestore to store the typing status of each user.

To implement typing indicators:

  1. Add a typing field to the user document in the Firestore database.
  2. Listen for changes in the typing field of other users and display a typing indicator when necessary.
const [typingUsers, setTypingUsers] = useState([]);

useEffect(() => {
  const unsubscribe = firebase
    .firestore()
    .collection('users')
    .doc(user.uid)
    .onSnapshot(doc => {
      const data = doc.data();
      setTypingUsers(data.typing || []);
    });

  return () => unsubscribe();
}, []);

In the useEffect hook, we listen for changes in the typing field of the user document and update the typingUsers state variable accordingly.

To display the typing indicator, we can add a condition in the render method to check if the user is typing.

{typingUsers.includes(message.userId) && <p>Typing...</p>}

With this implementation, the chat interface will display a "Typing..." message whenever a user is typing a message.

User presence status

User presence status can be added to show when a user is online or offline. We can use Firebase Realtime Database to store the presence status of each user.

To implement user presence status:

  1. Add a presence field to the user document in the Firestore database.
  2. Use Firebase Realtime Database to update the presence field when the user comes online or goes offline.
  3. Listen for changes in the presence field of other users and display their presence status.
const [presenceStatus, setPresenceStatus] = useState(null);

useEffect(() => {
  const userStatusRef = firebase.database().ref('status/' + user.uid);

  const setUserOnline = async () => {
    await userStatusRef.set({ online: true });
  };

  const setUserOffline = async () => {
    await userStatusRef.onDisconnect().set({ online: false });
  };

  setUserOnline();

  const presenceRef = firebase.database().ref('.info/connected');
  presenceRef.on('value', snapshot => {
    if (snapshot.val()) {
      setUserOnline();
    } else {
      setUserOffline();
    }
  });

  const presenceStatusRef = firebase.database().ref('status');
  presenceStatusRef.on('value', snapshot => {
    const data = snapshot.val();
    setPresenceStatus(data);
  });

  return () => {
    presenceRef.off();
    presenceStatusRef.off();
  };
}, []);

In the useEffect hook, we set up the necessary listeners to update the presence status of the user and listen for changes in the presence status of other users.

To display the presence status, we can add a condition in the render method to check if the user is online or offline.

<p>{presenceStatus && presenceStatus[message.userId]?.online ? 'Online' : 'Offline'}</p>

With this implementation, the chat interface will display the presence status of each user, updating in real-time as users come online or go offline.

Message reactions

Message reactions can be added to allow users to react to specific messages. We can use Firebase Firestore to store the reactions for each message.

To implement message reactions:

  1. Add a reactions field to the message document in the Firestore database.
  2. Listen for changes in the reactions field of each message and update the reactions accordingly.
const [reactions, setReactions] = useState({});

useEffect(() => {
  const unsubscribe = firebase
    .firestore()
    .collection('messages')
    .onSnapshot(snapshot => {
      const data = snapshot.docs.reduce((acc, doc) => {
        const message = doc.data();
        acc[doc.id] = message.reactions || [];
        return acc;
      }, {});
      setReactions(data);
    });

  return () => unsubscribe();
}, []);

In the useEffect hook, we listen for changes in the reactions field of each message and update the reactions state variable accordingly.

To add a reaction to a message, we can add a button in the render method and update the reactions field of the message document when the button is clicked.

const addReaction = async messageId => {
  const existingReactions = reactions[messageId] || [];
  const newReaction = prompt('Enter reaction:');
  if (newReaction === null || newReaction.trim() === '') return;

  const updatedReactions = [...existingReactions, newReaction];
  await firebase.firestore().collection('messages').doc(messageId).update({
    reactions: updatedReactions,
  });
};

{messages.map(message => (
  <div key={message.id}>
    <p>{message.text}</p>
    <button onClick={() => addReaction(message.id)}>React</button>
    <div>
      {reactions[message.id]?.map((reaction, index) => (
        <span key={index}>{reaction} </span>
      ))}
    </div>
  </div>
))}

With this implementation, the chat interface will display a "React" button for each message. When the button is clicked, the user will be prompted to enter a reaction, and the reaction will be added to the reactions field of the message document.

Deploying the app

To deploy the chat app, we can use Firebase Hosting. Firebase Hosting allows us to host and deploy our static assets, including our React app.

Preparing for deployment

Before deploying the app, we need to build the React app by running the following command in the terminal:

npm run build

This will create an optimized production-ready build of our app in the build directory.

Deploying to Firebase Hosting

To deploy the app to Firebase Hosting, follow these steps:

  1. Install the Firebase CLI by running the following command in the terminal:
npm install -g firebase-tools
  1. Log in to Firebase by running the following command and following the prompts:
firebase login
  1. Initialize Firebase in the project by running the following command and following the prompts:
firebase init
  1. Select the hosting option, choose the Firebase project you created earlier, and set the build directory as the public directory.

  2. Deploy the app by running the following command:

firebase deploy

After the deployment is complete, Firebase will provide a public URL for your app. You can share this URL with others to access the chat app.

Conclusion

In this tutorial, we have built a real-time chat app using React and Firebase. We started by setting up the project and initializing Firebase. Then, we built the chat interface, allowing users to send and receive messages in real-time. We added user authentication to secure the chat app and explored additional features such as typing indicators, user presence status, and message reactions. Finally, we deployed the app to Firebase Hosting, making it accessible to others.

By combining the power of React and Firebase, we have created a powerful and efficient chat app that updates in real-time. This app can serve as a foundation for building more complex real-time chat applications and can be customized and extended to meet specific requirements.