DEV Community

Cover image for Build a REALTIME CHAT APP in 5 MIN with React and Firebase
Grégory D'Angelo for AlterClass

Posted on • Originally published at alterclass.io

Build a REALTIME CHAT APP in 5 MIN with React and Firebase

In this video/article, I'll show you how to build a powerful real-time chat application like Discord or Slack in 5 MINUTES ONLY using React and Firebase.

You'll learn how to authenticate users with Google and work with real-time data using Cloud Firestore. Plus there is a BONUS at the end 😉

Let's go! 🚀 🔥

Watch the video on Youtube or keep reading.


Table of content


What are we building?

Before we get started, let's have a quick look at what we are building today.

It's a simple but powerful chat application that everybody can use to communicate using text messages. It's like a Discord server or a Slack room with a single channel.

To enter the chat and interact with people, the user must log in using his Google account.

Authentication page

Once logged in, the user can read the previous messages from the chat and start chatting by sending messages.

Chat page

All messages are stored in a Firestore database and are sync in real-time with our React application.

So let's get started! I'll set a timer, and in just 5 minutes, you'll have an up and running real-time chat application.

And remember, at the end of the video, I'll show you how to deploy your app for free in 3 easy steps! So stick with me till the end, and you'll get a lived application.

Create a React app

First of all, we'll need to create a new React application. For that, we are going to use the create-react-app tool to set up our application with just one command line.

So let's go ahead and run that command with the name of our application React FireChat:

npx create-react-app react-firechat
Enter fullscreen mode Exit fullscreen mode

Once it's done, we can install the dependency to work with Firebase, which is the Firebase JS SDK.

npm install --save firebase
Enter fullscreen mode Exit fullscreen mode

Now, let's go ahead and open our App.js file and remove all of the boilerplate code already in place.

function App() {
  return <div></div>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Then, import the Firebase SDK along with Firestore for our database and firebase auth for user authentication.

// Firebase deps
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
Enter fullscreen mode Exit fullscreen mode

Finally, we need to initialize Firebase in our app by passing in the Firebase project configuration. To get this configuration, we need a Firebase project. So let's create one.

firebase.initializeApp({ ... });
Enter fullscreen mode Exit fullscreen mode

Create a Firebase project

To create a new Firebase project, head over to the Firebase console, click "Add project," and follow through the steps.

Create a Firebase project

Then, to allow users to sign in using their Google account, we need to enable the Google sign-in method from the authentication page for our project.

Google sign-in method

And to use Firestore as our database for storing messages, we need to enable it from the Cloud Firestore page.

Cloud Firestore

Finally, the last thing we need to do is to add a new web app to our Firebase project. Once you are done, you should get a JavaScript object representing the configuration of the Firebase project application you just created.

Firebase

Firebase

Firebase

So go ahead and copy this configuration object and paste it into the initializeApp in your source code.

Alright! Now we should have everything we need to build our chat application. We created the React application, the Firebase project, and linked the two together.

So let's build the views of our application.

Define the UI

But first, let's see how our application is structured and break the UI down into a component hierarchy.

In this application, if the user is logged in, we show the chat with the list of messages. Otherwise, we show a button to sign in with Google.

React Firebase App Views

In addition to our main App component, we have 3 other React components to create. One for the sign-in and sign-out buttons, one for the channel that contains the list of messages, and the one for the messages themselves.

React Firebase App Components

Google authentication

Sign-in button

So let's start with the sign-in button.

This component is pretty straightforward. It is a simple button that accepts an onClick prop.

const Button = ({ onClick = null, children = null }) => (
  <button onClick={onClick}>{children}</button>
);

export default Button;
Enter fullscreen mode Exit fullscreen mode

From theApp component, we can now render it and implement the onClick event handler.

function App() {
  const signInWithGoogle = async () => {
    // Retrieve Google provider object
    const provider = new firebase.auth.GoogleAuthProvider();
    // Set language to the default browser preference
    firebase.auth().useDeviceLanguage();
    // Start sign in process
    try {
      await firebase.auth().signInWithPopup(provider);
    } catch (error) {
      console.log(error.message);
    }
  };

  return (
    <div>
      <Button onClick={signInWithGoogle}>Sign in with Google</Button>;
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this event handler, we are starting the sign-in process by using the Google Auth provider from Firebase, we then set the language to the user's preferred language, and we call the signInWithPopup method to prompt the user to sign in through a pop-up window.

Okay, now we need a way to check if the user is authenticated. In this case, we don't show the sign-in button, but instead, we let him enter the chat directly.

We can quickly get access to the authenticated user from Firebase through the auth.currentUser property.

const [user, setUser] = useState(() => auth.currentUser);
Enter fullscreen mode Exit fullscreen mode

But as with many applications, you will also want to know whether your users are currently signed-in or signed-out of your application. Firebase provides a method called onAuthStateChanged, which allows you to subscribe to the user's current authentication state and receive an event whenever that state changes.

Setting such an event listener is something we should do inside the useEffect React hook as it is a side effect.

So let's go ahead and set this up.

const [initializing, setInitializing] = useState(true);
const [user, setUser] = useState(() => auth.currentUser);

useEffect(() => {
  const unsubscribe = firebase.auth().onAuthStateChanged(user => {
    if (user) {
      setUser(user);
    } else {
      setUser(false);
    }
    if (initializing) {
      setInitializing(false);
    }
  });

  // Cleanup subscription
  return unsubscribe;
}, [initializing]);
Enter fullscreen mode Exit fullscreen mode

This method returns a function we can use to clean up our subscription whenever our component unmount.

One more thing, the onAuthStateChanged listener is asynchronous and will trigger an initial state once a connection with Firebase has been established.

Therefore, it is crucial to set up an "initializing" state that blocks our main application from rendering while the connection is established.

After a user signs in for the first time, a new user account is created and stored as part of your Firebase project. It can be used to identify a user across every app in your project, regardless of how the user signs in.

Firebase - Users

Sign-out button

Great! Now let's add a sign-out button to allow the user to sign out from our app.

To sign out a user, we can just call the signOut method from the Firebase SDK. And as we have set up a listener to listen for any authentication changes, the user state should be updated automatically and set to null, triggering a re-render of our app, which now display the sign-in button again.

function App() {
  const signInWithGoogle = async () => { ... };

  const signOut = async () => {
    try {
      await firebase.auth().signOut();
    } catch (error) {
      console.log(error.message);
    }
  };

  return (
    <div>
      {
        user ? (
          <>
            <Button onClick={signOut}>Sign out</Button>
            <p>Welcome to the chat!</p>
          </>
        ) : <Button onClick={signInWithGoogle}>Sign in with Google</Button>
      }
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Read data in realtime from Firestore

Now, let's jump into creating the Channel component.

This one is responsible for displaying the chat messages and an input field to allow the user to send new messages to the chat.

But first, we need to set up our Firestore database from the Firebase console.

Cloud Firestore stores data within "documents," which are contained within "collections." In our case, we'll store the chat messages of our users within a "Messages" collection.

Firestore collection

Let's go ahead and also create our first document within this collection.

For now, we just add two fields to this new document. The text of the message itself and a timestamp representing the date and time when the message has been created.

Firestore documents

Great! That's all we need. Let's go back to our application.

Within our Channel component, we need to query Firestore for retrieving the messages and render them onto the screen with JSX as usual. So let' start by creating a state variable to store our messages.

const Channel = ({ user = null }) => {
  const [messages, setMessages] = useState([]);
  return <ul></ul>;
};

export default Channel;
Enter fullscreen mode Exit fullscreen mode

Querying data is a side effect. So we'll fetch the messages from within the useEffect hook.

Cloud Firestore provides the ability to read the value of a collection or a document. This can be done in one shot or by listening to real-time updates when the data within a query changes.

In our case, we are only interested in real-time updates. The way we do it with Firestore is by setting up an active listener to react to any changes to the query we want to perform, using the onSnapshot method with an event handler callback.

To watch the "Messages" collection for any changes, we create the following query.

const db = firebase.firestore();
const query = db.collection('messages').orderBy('createdAt').limit(100);
Enter fullscreen mode Exit fullscreen mode

We get the reference to the collection, order it by the createdAt property, and finally, we limit the number of messages returned from the query.

This one is up to you. For simplicity here, we limit it to 100 messages. You can also use query cursors to read the messages by batches.

Now that we have our query, we can set up our event listener with the onSnapshot method. It also returns a function, allowing us to unsubscribe from events to clean up our side effects when the component unmounts.

useEffect(() => {
  // Subscribe to query with onSnapshot
  const unsubscribe = query.onSnapshot(querySnapshot => {
    ...
  });

  // Detach listener
  return unsubscribe;
}, []);
Enter fullscreen mode Exit fullscreen mode

Once a query has returned a result, Firestore returns a QuerySnapshot. These snapshots provide the ability to actually get the data we requested through our query.

To access all the documents returned by our query, we can just get it from the docs property of the QuerySnapshot and loop over it to get the data for each document alongside the document id.

useEffect(() => {
  // Subscribe to query with onSnapshot
  const unsubscribe = query.onSnapshot(querySnapshot => {
    // Get all documents from collection - with IDs
    const data = querySnapshot.docs.map(doc => ({
      ...doc.data(),
      id: doc.id,
    }));
  });

  // Detach listener
  return unsubscribe;
}, []);
Enter fullscreen mode Exit fullscreen mode

Finally, we update our state and render the messages.

useEffect(() => {
  // Subscribe to query with onSnapshot
  const unsubscribe = query.onSnapshot(querySnapshot => {
    // Get all documents from collection - with IDs
    const data = querySnapshot.docs.map(doc => ({
      ...doc.data(),
      id: doc.id,
    }));
    // Update state
    setDocs(data);
  });

  // Detach listener
  return unsubscribe;
}, []);
Enter fullscreen mode Exit fullscreen mode

So, if we create a new message in our Firestore collection, we should see it automatically appear on the screen.

// Channel.js
const Channel = ({ user = null }) => {
  const [messages, setMessages] = useState([]);

  useEffect(() => { ... });

  return (
    <ul>
      {messages.map(message => (
        <li key={message.id}>{message.text}</li>
      ))}
    </ul>
  );
};

// App.js
function App() {
  ...

  return (
    <div>
      {
        user ? (
          <>
            <Button onClick={signOut}>Sign out</Button>
            <Channel user={user} />
          </>
        ) : <Button onClick={signInWithGoogle}>Sign in with Google</Button>
      }
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Add data to Firestore

Alright! Now let's add the ability to create new messages from our application.

To achieve that, we can create a form with a single input field inside our Channel component.

const Channel = ({ user = null }) => {
  const [messages, setMessages] = useState([]);
  const [newMessage, setNewMessage] = useState('');

  ...

  const handleOnChange = e => {
    setNewMessage(e.target.value);
  };

  return (
    <>
      <ul>
        {messages.map(message => (
          <li key={message.id}>{message.text}</li>
        ))}
      </ul>
      <form
        onSubmit={handleOnSubmit}>
        <input
          ref={inputRef}
          type="text"
          value={newMessage}
          onChange={handleOnChange}
          placeholder="Type your message here..."
        />
        <button
          type="submit"
          disabled={!newMessage}
        >
          Send
        </button>
      </form>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

And implement the onSubmit event handler to add the new messages to our Firestore database.

It's actually pretty easy to do as we already have everything in place. Again we are using the messages collection reference from the Firebase SDK and call the add method by passing in an object representing the data of the new message.

const handleOnSubmit = e => {
  e.preventDefault();

  const trimmedMessage = newMessage.trim();
  if (trimmedMessage) {
    // Add new message in Firestore
    messagesRef.add({
      text: trimmedMessage,
      createdAt: firebase.firestore.FieldValue.serverTimestamp(),
      uid,
      displayName,
      photoURL,
    });
    // Clear input field
    setNewMessage('');
  }
};
Enter fullscreen mode Exit fullscreen mode

So we have the text itself, the createdAt timestamp - here, we let Firebase setting the value using a server-generated timestamp - and then we have the user ID, the user displayName, and his photoURL. We get this information from the parent component through props.

const { uid, displayName, photoURL } = user;
Enter fullscreen mode Exit fullscreen mode

Here you go! Now our users can post messages, and others can read them in real-time.

One more thing, now that we are attaching user information to each message, let's add the photo and name of the user next to each message on the chat.

To do so, we can create a last component called Message to render each individual message from within the Channel component.

// Message.js
const Message = ({
  createdAt = null,
  text = '',
  displayName = '',
  photoURL = '',
}) => {
  if (!text) return null;

  return <div></div>;
};

// Channel.js
const Channel = ({ user = null }) => {
  ...

  return (
    <>
      <ul>
        {messages.map(message => (
          <li key={message.id}>
            <Message {...message} />
          </li>
        ))}
      </ul>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

This component is responsible for formatting and rendering the message's data. So we have the user's photo and display name, the creation date that we convert to a more readable format using the date-fns library, and finally, we have the text of the message.

import { formatRelative } from 'date-fns';

const formatDate = date => {
  let formattedDate = '';
  if (date) {
    // Convert the date in words relative to the current date
    formattedDate = formatRelative(date, new Date());
    // Uppercase the first letter
    formattedDate =
      formattedDate.charAt(0).toUpperCase() + formattedDate.slice(1);
  }
  return formattedDate;
};

const Message = ({
  createdAt = null,
  text = '',
  displayName = '',
  photoURL = '',
}) => {
  if (!text) return null;

  return (
    <div>
      {photoURL ? (
        <img
          src={photoURL}
          alt="Avatar"
          className="rounded-full mr-4"
          width={45}
          height={45}
        />
      ) : null}
      {displayName ? <p>{displayName}</p> : null}
      {createdAt?.seconds ? (
        <span>{formatDate(new Date(createdAt.seconds * 1000))}</span>
      ) : null}
      <p>{text}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

So now you have a powerful real-time chat application built with React and Firebase!

React FireChat

I know what you think right now! The application looks terrible. There is no way our users gonna use it like that. So let's use our magic CSS wand and turn this awful app into something like this.

Chat page

Much better right?

That's not a video/article about CSS, so I'll save your time and energy and link the source code here so you can get through it.

I don't even use and write plain CSS anymore for my React applications. In fact, I'm using Tailwind CSS to rapidly add styles to my apps. It is one of those tools that once you start using it, you can't stop using it. I promise Tailwind CSS is such a life-changer.

Tailwind CSS

If you'd like me to create a video or a tutorial about it, please let me know in the comments section below.

And have a look at the Github repository for the source code of the app we just built.

Bonus

For learning how to deploy this app for free in 3 easy steps, go watch the video on YouTube, and see me going through the steps.

What's next?

Now I invite you to go a step further and add more features to it.

For example, you could implement an AI-powered moderation strategy and include it in your app to moderate users' messages automatically and ban the ones that break your community guidelines.

You could also add payments to your application using tools like Stripe, for example.

You got the point here. Possibilities are endless. This application is just a starting point. Grab the source code, and work from there to build your own application.

Conclusion

Alright! That's it, guys. Thank you so much for watching this video (or read the article) up to the very end.

I really hope that this video was useful for you guys. Please share it with your friends and colleagues or whoever may be interested in building applications with React.

And if you have not yet subscribed, don't forget to subscribe to AlterClass on Youtube, hit the like button, and ring the bell if you want to be notified every time I release a new video.

Thank you!

Become a React Developer

And if you need to learn more about building modern web applications with React, go check out my course on AlterClass.io.

AlterClass.io

My course will teach you everything you need to master React, become a successful React developer, and get hired!

AlterClass.io

I'll teach all the concepts you need to work with React, you'll get tons of hands-on practice through quizzes and programming assessments, and you'll build real-world projects on your own.

AlterClass.io

AlterClass.io

Plus, you'll be part of a growing community of learners.

So go to AlterClass.io, enroll in my course, and start building an amazing portfolio of powerful React applications.

Top comments (8)

Collapse
 
eltel profile image
Terry Mitchell

Hi... I followed along, but due to the lack of filenames in this tut, I wasn't sure where to put most of the functions you mentioned. As the repo on Github has little relation to the code discussed here I'm a little confused about how to actually get this to work. Did you share the correct repo?

Collapse
 
eltel profile image
Terry Mitchell

it appears that your repo doesn't work either (with my firebase config) - any idea what's wrong? I've had a lot of experience with Firebase and initially, your app seemed simple but there appears to be something fundamental missing from this tutorial - any advice/updates greatly appreciated.

Thanks
Terry

Thanksthet
Terry

Collapse
 
eltel profile image
Terry Mitchell

OK, after focussing on the Firestore rules I finally got it to work - I think this should have been covered more in-depth in this tut - but thanks overall, working great!!

Collapse
 
ferakoozy profile image
ferakoozy

please i will like to get the complete file of this app, because the one on github is not complete please help me with it.. by the way cool app...

Collapse
 
luismartinezs profile image
Luis Martinez Suarez

Just installing firebase alone is already taking 5 minutes hahaha

Collapse
 
yashshah224 profile image
Yash Shah

Nice 👍

Collapse
 
andrewbaisden profile image
Andrew Baisden

Wow impressive!

Collapse
 
rv90904 profile image
Ramesh Vishnoi

Nice Tutorial