Building a chat app with React and Firebase (Part 1)

In this multi-part series, I will be building a full-featured chat application with react and Firebase. The first entry in the series will focus on building a simple global chat room where all the messages will be readable by all members. Member authentication will be handled using Google Sign-in.

Prerequisites

Before we start, ensure you have:

  1. node and npm or yarn installed on your computer
  2. Access to a Firebase account

Bootstrapping the react project

We will use typescript for our react project. We will need the firebase and react-firebase-hooks packages to interact with the Firebase project. The react-env package will be useful for loading environmental variables from a .env file

To initialize a React project with typescript and install our dependencies open a terminal and run

npx create-react-app react-chat-app --template typescript
cd react-chat-app
npm install  --save firebase react-firebase-hooks

Create Firebase App

Log in to your Firebase Console and click on Add Project. Give your new project a name, check the confirmation checkbox and click on Continue.

image.png

Uncheck Enable Google Analytics for this project for a faster setup, and then click Create project, and then wait for the project to be created.

image.png

Once the project is ready, click on Continue to go to the project home.

image.png

To allow users to login with Google Sign-in, click on Authentication then Get started.

image.png

Under the Sign-in method tab, select Google

image.png

Click on the Enable switch and select a support email, then click on Save.

image.png

To configure a Cloud Firestore database, go back to the project home and click on Cloud Firestore then Create database

image.png

Select Start in test mode, click on Next, select a location and click on Enable.

image.png

To include Firebase configurations in your application, go back to the project home and click on the web icon. image.png image.png

Fill in the information by following the prompts

image.png

Copy the configuration details and put them in a .env file in the root of your project in the format

FIREBASE_API_KEY=*****************************************
FIREBASE_AUTH_DOMAIN=**********************************
FIREBASE_DATABASE_URL=**********************************
FIREBASE_PROJECT_ID=******************
FIREBASE_STORAGE_BUCKET=******************************
FIREBASE_MESSAGING_SENDER_id=*************
FIREBASE_APP_ID=******************************************

image.png

Firebase Configuration

We will handle all our applications Firebase concerns in the file src/firebase.ts.

First we import all the requirements for working with the Firestore database and Google Sign-in.

/* src/firebase.ts */
import {
  addDoc,
  collection,
  getFirestore,
  limit,
  orderBy,
  query,
  serverTimestamp,
} from "firebase/firestore";
import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import env from "react-dotenv";
import { initializeApp } from "firebase/app";

We then load the firebase configuration from the environmental variables. Next we initialize a firebase app, and get the auth and firestore variables for working with authentication and the database respectively.

/* src/firebase.ts */

const firebaseConfig = {
  apiKey: env.FIREBASE_API_KEY,
  authDomain: env.FIREBASE_AUTH_DOMAIN,
  databaseURL: env.FIREBASE_DATABASE_URL,
  projectId: env.FIREBASE_PROJECT_ID,
  storageBucket: env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: env.FIREBASE_MESSAGING_SENDER_id,
  appId: env.FIREBASE_APP_ID,
};

const firebaseApp = initializeApp(firebaseConfig);
export const auth = getAuth(firebaseApp);
export const firestore = getFirestore(firebaseApp);

The next step is to define exports that handle backend functionality:

  1. Signing in with google - We use a popup to allow the user to sign in with Google
  2. Sending a message - We allow a logged in user to create a message in the database.
  3. Retrieving the messages - We query the database for 25 messages, with the most recent first.
/* src/firebase.ts */

export const signInWithGoogle = async () => {
  const provider = new GoogleAuthProvider();
  return await signInWithPopup(auth, provider);
};

export const sendMessage = async (text: string) => {
  const { uid, photoURL } = auth.currentUser!;
  await addDoc(collection(firestore, "messages"), {
    text,
    createdAt: serverTimestamp(),
    uid,
    photoURL,
  });
};

export const publicMessagesQuery = query(
  collection(firestore, "messages"),
  orderBy("createdAt"),
  limit(25)
);

If the user is signed in, the application will show a Sign Out button, as well as the public chatroom. Otherwise, the application will show a Sign In button

We use the useAuthState hook provided by the react-firebase-hooks to keep track of the authentication status of the application.

The code in App.tsx is therefore:

/* src/App.tsx */

import React from "react";
import "./App.css";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth } from "./firebase";
import { SignIn, SignOut } from "./UserAuth";
import { ChatRoom } from "./ChatRoom";

function App() {
  const [user] = useAuthState(auth);
  return (
    <div className="App">
      <header>
        <SignOut />
      </header>
      <section>{user ? <ChatRoom /> : <SignIn />}</section>
    </div>
  );
}

export default App;

The code for Signing in and Signing out the user is inside src/UserAuth.tsx

/* src/UserAuth.tsx */
import { auth, signInWithGoogle } from "./firebase";
import React from "react";

export function SignOut() {
  return auth.currentUser && <button onClick={auth.signOut}>Sign Out</button>;
}

export function SignIn() {
  return <button onClick={signInWithGoogle}>Sign in with Google</button>;
}

Inside the ChatRoom component, we need a form for sending new messages as well as the list of messages to be displayed.

/* src/ChatRoom.tsx */

import React from "react";
import { useCollectionData } from "react-firebase-hooks/firestore";

import { publicMessagesQuery } from "./firebase";
import { MessageList } from "./MessageList";
import { ChatMessage } from "./types";
import { NewMessageForm } from "./NewMessageForm";

export function ChatRoom() {
  const [messages] = useCollectionData(publicMessagesQuery);

  return (
    <div>
      <MessageList messages={messages as ChatMessage[]} />
      <NewMessageForm />
    </div>
  );
}

The MessageList component receives a messages prop and renders each messages in a MessageItem component

/* src/MessageList.tsx */

import React from "react";
import { MessageItem } from "./MessageItem";
import { ChatMessage } from "./types";

export function MessageList({ messages }: { messages: ChatMessage[] }) {
  return (
    <>
      <main>
        {messages &&
          messages.map((msg) => (
            <MessageItem key={`${msg.createdAt}`} message={msg} />
          ))}
      </main>
    </>
  );
}

The MessageItem component renders a single message with the avatar of the user who sent it

/* src/MessageItem.tsx */

import React from "react";
import { auth } from "./firebase";
import { ChatMessage } from "./types";

export function MessageItem({ message }: { message: ChatMessage }) {
  const { text, uid, photoURL } = message;
  const messageClass = uid === auth?.currentUser?.uid ? "sent" : "received";

  return (
    <div className={`message ${messageClass}`}>
      <img src={photoURL} alt="user avatar" />
      <p>{text}</p>
    </div>
  );
}

The NewMessageForm component allows the user to type a message and send it to the database. Hitting the Submit button or Enter key submits the message. To create a newline, the key combination is Shift + Enter.

/* src/NewMessageForm.tsx */
import { sendMessage } from "./firebase";
import React, { useState } from "react";

export function NewMessageForm() {
  const [formValue, setFormValue] = useState("");

  function addMessageEnter(event: React.KeyboardEvent) {
    if (event.key === "Enter" && event.shiftKey === false) {
      submit(formValue);
    }
  }

  function submit(message: string) {
    if (message.trim() !== "") {
      sendMessage(message);
    }
    setFormValue("");
  }

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        submit(formValue);
      }}
    >
      <div className="chat-message clearfix">
        <textarea
          required={true}
          value={formValue}
          onKeyUp={addMessageEnter}
          onChange={(e) => setFormValue(e.target.value)}
        />
        <button type="submit">Submit</button>
      </div>
    </form>
  );
}

Conclusion

In this part of the series, we created a basic chat app that allows users to send messages to a common chat room. In the next part of the series, we will extend the functionality to allow private inboxes between two users.