Building a Real-Time Chat App with React and Firebase

Publikováno: 23.3.2020

In this article, we’ll cover key concepts for authenticating a user with Firebase in a real-time chat application. We’ll integrate third-party auth providers (e.g. Google, Twitter and GitHub) and, once users are signed in, we’ll learn how to store user chat data in the Firebase Realtime Database, where we can sync data with a NoSQL cloud database.

The client application is going to be built in React, as it is one of the most popular JavaScript frameworks out … Read article “Building a Real-Time Chat App with React and Firebase”

The post Building a Real-Time Chat App with React and Firebase appeared first on CSS-Tricks.

Celý článek

In this article, we’ll cover key concepts for authenticating a user with Firebase in a real-time chat application. We’ll integrate third-party auth providers (e.g. Google, Twitter and GitHub) and, once users are signed in, we’ll learn how to store user chat data in the Firebase Realtime Database, where we can sync data with a NoSQL cloud database.

The client application is going to be built in React, as it is one of the most popular JavaScript frameworks out there, but the concepts can also be applied to other frameworks.

But first, what is Firebase?

Firebase is Google's mobile platform for quickly developing apps. Firebase provides a suite of tools for authenticating applications, building reactive client apps, reporting analytics, as well as a host of other helpful resources for managing apps in general. It also provides back-end management for web, iOS, Android, and Unity, a 3D development platform.

Out of the box, Firebase is packaged with features that help developers like ourselves focus on building apps while it handles all server-side logic. Things like:

  • Authentication: This includes support for email and password authentication as well as single sign-on capabilities (via Facebook, Twitter and Google).
  • Realtime database: This is a “NoSQL” database that updates in real time.
  • Cloud functions: These run extra server-side logic.
  • Static hosting: This is a means of serving assets pre-built instead of rendering at runtime.
  • Cloud storage: This gives us a place to store media assets.

Firebase offers a generous free tier that includes authentication and access to their Realtime Database. The authentication providers we’ll be covering email and password — Google and GitHub — are free on that side as well. The Realtime Database allows up to 100 simultaneous connections and 1 gigabyte storage per month. A full table of pricing can be found on the Firebase website.

Here’s what we’re making

We’re going to build an application called Chatty. It will allow only authenticated users to send and read messages and users can sign up by providing their email and creating a password, or by authenticating through a Google or GitHub account. Check out source code if you want to refer to it or take a peek as we get started.

We’ll end up with something like this:

Setting up

You’re going to need a Google account to use Firebase, so snag one if you haven’t already. And once you do, we can officially kick the tires on this thing.

First off, head over to the Firebase Console and click the “Add project" option.

Next, let’s enter a name for the project. I’m going with Chatty.

You can choose to add analytics to your project, but it’s not required. Either way, click continue to proceed and Firebase will take a few seconds to delegate resources for the project.

Once that spins up, we are taken to the Firebase dashboard But, before we can start using Firebase in our web app, we have to get the configuration details down for our project. So, click on the web icon in the dashboard.

Then, enter a name for the app and click Register app.

Next up, we’ll copy and store the configuration details on the next screen in a safe place. That will come in handy in the next step.

Again, we’re going to authenticate users via email and password, with additional options for single sign-on with a Google or GitHub account. We need to enable these from the Authentication tab in the dashboard, but we’ll go through each of them one at a time.

Email and password authentication

There’s a Sign-in method tab in the Firebase dashboard. Click the Email/Password option and enable it.

Now we can use it in our app!

Setting up the web app

For our web app, we’ll be using React but most of the concepts can be applied to any other framework. Well need Node.js for a React setup, so download and install it if you haven’t already.

We’ll use create-react-app to bootstrap a new React project. This downloads and installs the necessary packages required for a React application. In the terminal, cd into where you’d like our Chatty project to go and run this to initialize it:

npx create-react-app chatty

This command does the initial setup for our react app and installs the dependencies in package.json. We’ll also install some additional packages. So, let’s cd into the project itself and add packages for React Router and Firebase.

cd chatty
yarn add react-router-dom firebase

We already know why we need Firebase, but why React Router? Our chat app will have a couple of views we can use React Router to handle navigating between pages.

With that done, we can officially start the app:

yarn start

This starts a development server and opens a URL in your default browser. If everything got installed correctly, you should see a screen like this:

Looking at the folder structure, you would see something similar to this:

For our chat app, this is the folder structure we’ll be using:

  • /components: contains reusable widgets used in different pages
  • /helpers: a set of reusable functions
  • /pages: the app views
  • /services: third-party services that we’re using (e.g. Firebase)
  • App.js: the root component

Anything else in the folder is unnecessary for this project and can safely be removed. From here, let’s add some code to src/services/firebase.js so the app can talk with Firebase.

import firebase from 'firebase';

Let’s get Firebase into the app

We’ll import and initialize Firebase using the configuration details we copied earlier when registering the app in the Firebase dashboard. Then, we’ll export the authentication and database modules.

const config = {
  apiKey: "ADD-YOUR-DETAILS-HERE",
  authDomain: "ADD-YOUR-DETAILS-HERE",
  databaseURL: "ADD-YOUR-DETAILS-HERE"
};
firebase.initializeApp(config);
export const auth = firebase.auth;
export const db = firebase.database();

Let’s import our dependencies in src/App.js:

import React, { Component } from 'react';
import {
  Route,
  BrowserRouter as Router,
  Switch,
  Redirect,
} from "react-router-dom";
import Home from './pages/Home';
import Chat from './pages/Chat';
import Signup from './pages/Signup';
import Login from './pages/Login';
import { auth } from './services/firebase';

These are ES6 imports. Specifically, we’re importing React and other packages needed to build out the app. We’re also importing all the pages of our app that we’ll configure later to our router.

Next up is routing

Our app has public routes (accessible without authentication) and a private route (accessible only with authentication). Because React doesn’t provide a way to check the authenticated state, we’ll create higher-order components (HOCs) for both types of routes.

Our HOCs will:

  • wrap a <Route>,
  • pass props from the router to the <Route>,
  • render the component depending on the authenticated state, and
  • redirect the user to a specified route if the condition is not met

Let’s write the code for our <PrivateRoute> HOC.

function PrivateRoute({ component: Component, authenticated, ...rest }) {
  return (
    <Route
      {...rest}
      render={(props) => authenticated === true
        ? <Component {...props} />
        : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}
    />
  )
}

It receives three props: the component to render if the condition is true, the authenticated state, and the ES6 spread operator to get the remaining parameters passed from the router. It checks if authenticated is true and renders the component passed, else it redirects to/login.

function PublicRoute({ component: Component, authenticated, ...rest }) {
  return (
    <Route
      {...rest}
      render={(props) => authenticated === false
        ? <Component {...props} />
        : <Redirect to='/chat' />}
    />
  )
}

The <PublicRoute> is pretty much the same. It renders our public routes and redirects to the /chat path if the authenticated state becomes true. We can use the HOCs in our render method:

render() {
  return this.state.loading === true ? <h2>Loading...</h2> : (
    <Router>
      <Switch>
        <Route exact path="/" component={Home}></Route>
        <PrivateRoute path="/chat" authenticated={this.state.authenticated} component={Chat}></PrivateRoute>
        <PublicRoute path="/signup" authenticated={this.state.authenticated} component={Signup}></PublicRoute>
        <PublicRoute path="/login" authenticated={this.state.authenticated} component={Login}></PublicRoute>
      </Switch>
    </Router>
  );
}

Checking for authentication

It would be nice to show a loading indicator while we verify if the user is authenticated. Once the check is complete, we render the appropriate route that matches the URL. We have three public routes — <Home>, <Login> and <Signup> — and a private one called <Chat>.

Let’s write the logic to check if the user is indeed authenticated.

class App extends Component {
  constructor() {
    super();
    this.state = {
      authenticated: false,
      loading: true,
    };
  }
}

export default App;

Here we’re setting the initial state of the app. Then, we’re using the componentDidMountlifecycle hook to check if the user is authenticated. So, let’s add this after the constructor:

componentDidMount() {
  this.removelistener = auth().onAuthStateChanged((user) => {
    if (user) {
      this.setState({
        authenticated: true,
        loading: false,
      });
    } else {
      this.setState({
        authenticated: false,
        loading: false,
      });
    }
  })
}

Firebase provides an intuitive method called onAuthStateChanged that is triggered when the authenticated state changes. We use this to update our initial state. user is null if the user is not authenticated. If the user is true, we update authenticated to true; else we set it to false. We also set loading to false either way.

Registering users with email and password

Users will be able to register for Chatty through email and password. The helpers folder contains a set of methods that we’ll use to handle some authentication logic. Inside this folder, let’s create a new file called auth.js and add this:

import { auth } from "../services/firebase";

We import the auth module from the service we created earlier.

export function signup(email, password) {
  return auth().createUserWithEmailAndPassword(email, password);
}


export function signin(email, password) {
  return auth().signInWithEmailAndPassword(email, password);
}

We have two methods here: signup andsignin:

  • signup will create a new user using their email and password. 
  • signin will log in an existing user created with email and password.

Let’s create our <Signup> page by creating a new file Signup.js file in the pages folder. This is the markup for the UI:

import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { signup } from '../helpers/auth';


export default class SignUp extends Component {


  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <h1>
            Sign Up to
          <Link to="/">Chatty</Link>
          </h1>
          <p>Fill in the form below to create an account.</p>
          <div>
            <input placeholder="Email" name="email" type="email" onChange={this.handleChange} value={this.state.email}></input>
          </div>
          <div>
            <input placeholder="Password" name="password" onChange={this.handleChange} value={this.state.password} type="password"></input>
          </div>
          <div>
            {this.state.error ? <p>{this.state.error}</p> : null}
            <button type="submit">Sign up</button>
          </div>
          <hr></hr>
          <p>Already have an account? <Link to="/login">Login</Link></p>
        </form>
      </div>
    )
  }
}
Email? Check. Password? Check. Submit button? Check. Our form is looking good.

The form and input fields are bound to a method we haven’t created yet, so let’s sort that out. Just before the render() method, we’ll add the following:

constructor(props) {
  super(props);
  this.state = {
    error: null,
    email: '',
    password: '',
  };
  this.handleChange = this.handleChange.bind(this);
  this.handleSubmit = this.handleSubmit.bind(this);
}

We’re setting the initial state of the page. We’re also binding the handleChange and handleSubmit methods to the component’s this scope.

handleChange(event) {
  this.setState({
    [event.target.name]: event.target.value
  });
}

Next up, we’ll add the handleChange method that our input fields are bound to. The method uses computed properties to dynamically determine the key and set the corresponding state variable.

async handleSubmit(event) {
  event.preventDefault();
  this.setState({ error: '' });
  try {
    await signup(this.state.email, this.state.password);
  } catch (error) {
    this.setState({ error: error.message });
  }
}

For handleSubmit, we’re preventing the default behavior for form submissions (which simply reloads the browser, among other things). We’re also clearing up the error state variable, then using the signup() method imported from helpers/auth to pass the email and password entered by the user.

If the registration is successful, users get redirected to the /Chats route. This is possible with the combination of onAuthStateChanged and the HOCs we created earlier. If registration fails, we set the error variable which displays a message to users.

Authenticating users with email and password

The login page is identical to the signup page. The only difference is we’ll be using the signin method from the helpers we created earlier. That said, let’s create yet another new file in the pages directory, this time called Login.js, with this code in it:

import React, { Component } from "react";
import { Link } from "react-router-dom";
import { signin, signInWithGoogle, signInWithGitHub } from "../helpers/auth";


export default class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      error: null,
      email: "",
      password: ""
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }


  handleChange(event) {
    this.setState({
      [event.target.name]: event.target.value
    });
  }


  async handleSubmit(event) {
    event.preventDefault();
    this.setState({ error: "" });
    try {
      await signin(this.state.email, this.state.password);
    } catch (error) {
      this.setState({ error: error.message });
    }
  }


  render() {
    return (
      <div>
        <form
          autoComplete="off"
          onSubmit={this.handleSubmit}
        >
          <h1>
            Login to
            <Link to="/">
              Chatty
            </Link>
          </h1>
          <p>
            Fill in the form below to login to your account.
          </p>
          <div>
            <input
              placeholder="Email"
              name="email"
              type="email"
              onChange={this.handleChange}
              value={this.state.email}
            />
          </div>
          <div>
            <input
              placeholder="Password"
              name="password"
              onChange={this.handleChange}
              value={this.state.password}
              type="password"
            />
          </div>
          <div>
            {this.state.error ? (
              <p>{this.state.error}</p>
            ) : null}
            <button type="submit">Login</button>
          </div>
          <hr />
          <p>
            Don't have an account? <Link to="/signup">Sign up</Link>
          </p>
        </form>
      </div>
    );
  }
}

Again, very similar to before. When the user successfully logs in, they’re redirected to /chat.

Authenticating with a Google account

Firebase allows us to authenticate users with a valid Google account. We’ve got to enable it in the Firebase dashboard just like we did for email and password.

Select the Google option and enable it in the settings.

On that same page, we also need to scroll down to add a domain to the list of domains that are authorized to access feature. This way, we avoid spam from any domain that is not whitelisted. For development purposes, our domain is localhost, so we’ll go with that for now.

We can switch back to our editor now. We’ll add a new method to helpers/auth.js to handle Google authentication.

export function signInWithGoogle() {
  const provider = new auth.GoogleAuthProvider();
  return auth().signInWithPopup(provider);
}

Here, we’re creating an instance of the GoogleAuthProvider. Then, we’re calling signInWithPopup with the provider as a parameter. When this method is called, a pop up will appear and take the user through the Google sign in flow before redirecting them back to the app. You’ve likely experienced it yourself at some point in time.

Let’s use it in our signup page by importing the method:

import { signin, signInWithGoogle } from "../helpers/auth";

Then, let’s add a button to trigger the method, just under the Sign up button:

<p>Or</p>
<button onClick={this.googleSignIn} type="button">
  Sign up with Google
</button>

Next, we’ll add the onClick handler:

async googleSignIn() {
  try {
    await signInWithGoogle();
  } catch (error) {
    this.setState({ error: error.message });
  }
}

Oh, and we should remember to bind the handler to the component:

constructor() {
  // ...
  this.githubSignIn = this.githubSignIn.bind(this);
}

That’s all we need! When the button is clicked, it takes users through the Google sign in flow and, if successful, the app redirects the user to the chat route.

Authenticating with a GitHub account

We’re going to do the same thing with GitHub. May as well give folks more than one choice of account.

Let’s walk through the steps. First, we’ll enable GitHub sign in on Firebase dashboard, like we did for email and Google.

You will notice both the client ID and client secret fields are empty, but we do have our authorization callback URL at the bottom. Copy that, because we’ll use it when we do our next thing, which is register the app on GitHub.

Once that’s done, we’ll get a client ID and secret which we can now add to the Firebase console.

Let’s switch back to the editor and add a new method to helpers/auth.js:

export function signInWithGitHub() {
  const provider = new auth.GithubAuthProvider();
  return auth().signInWithPopup(provider);
}

It’s similar to the Google sign in interface, but this time we’re creating a GithubAuthProvider. Then, we’ll call signInWithPopup with the provider.

In pages/Signup.js, we update our imports to include the signInWithGitHub method:

import { signup, signInWithGoogle, signInWithGitHub } from "../helpers/auth";

We add a button for GitHub sign up:

<button type="button" onClick={this.githubSignIn}>
  Sign up with GitHub
</button>

Then we add a click handler for the button which triggers the GitHub sign up flow:

async githubSignIn() {
  try {
    await signInWithGitHub();
  } catch (error) {
    this.setState({ error: error.message });
  }
}

Let’s remember again to bind the handler to the component:

constructor() {
  // ...
  this.githubSignIn = this.githubSignIn.bind(this);
}

Now we’ll get the same sign-in and authentication flow that we have with Google, but with GitHub.

Reading data from Firebase

Firebase has two types of databases: A product they call Realtime Database and another called Cloud Firestore. Both databases are NoSQL-like databases, meaning the database is structured as key-value pairs. For this tutorial, we’ll use the Realtime Database.

This is the structure we’ll be using for our app. We have a root node chats with children nodes. Each child has a content, timestamp, and user ID. One of the tabs you’ll notice is Rules which is how we set permissions on the contents of the database.

Firebase database rules are defined as key-value pairs as well. Here, we’ll set our rules to allow only authenticated users to read and write to the chat node. There are a lot more firebase rules. worth checking out.

Let’s write code to read from the database. First, create a new file called Chat.js  in the pages  folder and add this code to import React, Firebase authentication, and Realtime Database:

import React, { Component } from "react";
import { auth } from "../services/firebase";
import { db } from "../services/firebase"

Next, let’s define the initial state of the app:

export default class Chat extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: auth().currentUser,
      chats: [],
      content: '',
      readError: null,
      writeError: null
    };
  }
  async componentDidMount() {
    this.setState({ readError: null });
    try {
      db.ref("chats").on("value", snapshot => {
        let chats = [];
        snapshot.forEach((snap) => {
          chats.push(snap.val());
        });
        this.setState({ chats });
      });
    } catch (error) {
      this.setState({ readError: error.message });
    }
  }
}

The real main logic takes place in componentDidMount. db.ref("chats") is a reference to the chats path in the database. We listen to the value event which is triggered anytime a new value is added to the chats node. What is returned from the database is an array-like object that we loop through and push each object into an array. Then, we set the chats state variable to our resulting array. If there is an error, we set the readError state variable to the error message.

One thing to note here is that a connection is created between the client and our Firebase database because we used the .on() method. This means any time a new value is added to the database, the client app is updated in real-time which means users can see new chats without a page refresh Nice!.

After componentDidMount, we can render our chats like so:

render() {
  return (
    <div>
      <div className="chats">
        {this.state.chats.map(chat => {
          return <p key={chat.timestamp}>{chat.content}</p>
        })}
      </div>
      <div>
        Login in as: <strong>{this.state.user.email}</strong>
      </div>
    </div>
  );
}

This renders the array of chats. We render the email of the currently logged in user.

Writing data to Firebase

At the moment, users can only read from the database but are unable to send messages. What we need is a form with an input field that accepts a message and a button to send the message to the chat.

So, let’s modify the markup like so:

return (
    <div>
      <div className="chats">
        {this.state.chats.map(chat => {
          return <p key={chat.timestamp}>{chat.content}</p>
        })}
      </div>
      {# message form #}
      <form onSubmit={this.handleSubmit}>
        <input onChange={this.handleChange} value={this.state.content}></input>
        {this.state.error ? <p>{this.state.writeError}</p> : null}
        <button type="submit">Send</button>
      </form>
      <div>
        Login in as: <strong>{this.state.user.email}</strong>
      </div>
    </div>
  );
}

We have added a form with an input field and a button. The value of the input field is bound to our state variable content and we call handleChange when its value changes.

handleChange(event) {
  this.setState({
    content: event.target.value
  });
}

handleChange gets the value from the input field and sets on our state variable. To submit the form, we call handleSubmit:

async handleSubmit(event) {
  event.preventDefault();
  this.setState({ writeError: null });
  try {
    await db.ref("chats").push({
      content: this.state.content,
      timestamp: Date.now(),
      uid: this.state.user.uid
    });
    this.setState({ content: '' });
  } catch (error) {
    this.setState({ writeError: error.message });
  }
}

We set any previous errors to null. We create a reference to the chats node in the database and use push() to create a unique key and pushe the object to it.

As always, we have to bind our methods to the component:

constructor(props) {
  // ...
  this.handleChange = this.handleChange.bind(this);
  this.handleSubmit = this.handleSubmit.bind(this);
}

Now a user can add new messages to the chats and see them in real-time! How cool is that?

Demo time!

Enjoy your new chat app!

Congratulations! You have just built a chat tool that authenticates users with email and password, long with options to authenticate through a Google or GitHub account.

I hope this give you a good idea of how handy Firebase can be to get up and running with authentication on an app. We worked on a chat app, but the real gem is the signup and sign-in methods we created to get into it. That’s something useful for many apps.

Questions? Thoughts? Feedback? Let me know in the comments!

The post Building a Real-Time Chat App with React and Firebase appeared first on CSS-Tricks.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace