This article covers how to authenticate your web app with React and Firebase.

Sometimes you want to build a quick prototype without worrying about backend logic and infrastructure.

This is where Firebase really shines and combined with React, you can get authentication set up really quickly.

To start using Firebase, you need to create a Firebase project. I wrote the steps to create a Firebase project in a previous article.

You can follow the steps there to get your project configuration details.

Firebase has several authentication methods but in this article, we’ll be sticking to email/password sign-in.

Let’s dive in.

Authenticating React App with Firebase

Before we start building out our components, let’s take a moment to look at the structure for our app.

folder structure
  • components: contains reusable widgets used on different pages
  • helpers: a set of reusable functions
  • pages: the pages of our app
  • services: third-party services used by our app (e.g. Firebase)
  • App.js: root component of our app

You can create a new React app with create-react-app:

npx create-react-app my-app

On successful completion, install the react-router-dom and firebase packages.

cd my-app
yarn add firebase react-router-dom

Let’s add some code to src/services/firebase.js.

import firebase from 'firebase';

We import and initialize firebase. Then export the authentication module.

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;

Let’s use this 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 Profile from './pages/Profile';
import Signup from './pages/Signup';
import Login from './pages/Login';
import { auth } from './services/firebase';

We import packages and the pages of our app. We also import the auth module exported by our Firebase service.

Our app has public routes (accessible without authentication) and a private route (accessible only with authentication). The default Route exposed by the router does not provide a way to render a component only if the user is authenticated.

Higher-Order components

In this tutorial, we will be using Higher-order components that are not commonly used in modern React code.

We’ll create Higher-Order components (HOCs) for both types of routes.

Our HOCs will do the following:

  1. Wrap a <Route>
  2. Pass props from the router to the <Route>
  3. Render the component depending on the authenticated state
  4. Redirect 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 3 props: the component to render if the condition is true, the authenticated state and we use 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='/profile' />}
    />
  )
}

The <PublicRoute> is pretty much the same. It renders our public routes and redirects to the /profile 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="/profile" authenticated={this.state.authenticated} component={Profile}></PrivateRoute>
        <PublicRoute path="/signup" authenticated={this.state.authenticated} component={Signup}></PublicRoute>
        <PublicRoute path="/login" authenticated={this.state.authenticated} component={Login}></PublicRoute>
      </Switch>
    </Router>
  );
}

We show a loading indicator while checking if the user is authenticated or not. Once the check is complete, we render the appropriate route that matches the URL. We have 3 public routes – <Home>, <Login> and <Signup> – and one private route <Profile>.

Let’s write the logic to check if the user is authenticated. First, we set our initial state:

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

Then in componentDidMount, add the following:

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

Firebase gives us a very handy observer onAuthStateChanged that is triggered when the authenticated state changes and is the perfect place to update our state.

user is null if the user is not authenticated.

If user is true, we update authenticated to true else we set it to false.

We also set loading to false either way.

And that’s all we need to get our routing set up.

Register users with Firebase

We are using email/password registration which means our <Signup> component would be a form with input fields for email and password. Then a button to submit the form. 

Before we create the <Signup> component, let’s write our helper functions.

// src/helpers/auth.js
import { auth } from '../services/firebase';

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

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

export function logout() {
  return auth().signOut()
}

We import the auth method exposed by our Firebase service. Then we create signin, signup and logout methods. signin and signup both take in email and password as parameters and call the appropriate Firebase methods.

Inside src/pages/Signup.js, add the following:

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

export default class SignUp extends Component {

  constructor() {
    super();
    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 signup(this.state.email, this.state.password);
    } catch (error) {
      this.setState({ error: error.message });
    }
  }

  render() {
    return (
      <div className="container">
        <form className="mt-5 py-5 px-5" autoComplete="off" onSubmit={this.handleSubmit}>
          <h1>
            Sign Up to
          <Link className="title" to="/">ABC</Link>
          </h1>
          <p className="lead">Fill in the form below to create an account.</p>
          <div className="form-group">
            <input className="form-control" placeholder="Email" name="email" type="email" onChange={this.handleChange} value={this.state.email}></input>
          </div>
          <div className="form-group">
            <input className="form-control" placeholder="Password" name="password" onChange={this.handleChange} value={this.state.password} type="password"></input>
          </div>
          <div className="form-group">
            {this.state.error ? <p className="text-danger">{this.state.error}</p> : null}
            <button className="btn btn-primary rounded-pill px-5">Sign up</button>
          </div>
          <hr></hr>
          <p>Already have an account? <Link to="/login">Login</Link></p>
        </form>
      </div>
    )
  }
}

The code is relatively long so let’s walk through it.

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

Regular imports, including the signup method created prior to this step.

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

This sets up the initial state of the app.

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

Here we use the computed properties to dynamically determine the object key and set its value.

Note: the input field name attribute matches our state variables:

<input className="form-control" placeholder="Email" name="email" type="email" onChange={this.handleChange} value={this.state.email}></input>
<input className="form-control" placeholder="Password" name="password" onChange={this.handleChange} value={this.state.password} type="password"></input>


handleSubmit is where most of the magic happens.

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

We try to register the user. If there is an error, we set our error variable to the error message. If you remember in the beginning, we set an observer on the auth state so once the user successfully registers the app automatically redirects to the /profile route.

The rest of the component is the markup.

Login users with Firebase

Our <Login> component is basically the same.

// src/pages/Login.js
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { signin } from '../helpers/auth';

export default class Login extends Component {

  constructor() {
    super();
    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 className="container">
        <form className="mt-5 py-5 px-5" autoComplete="off" onSubmit={this.handleSubmit}>
          <h1>
            Login to
          <Link className="title" to="/">ABC</Link>
          </h1>
          <p className="lead">Fill in the form below to login to your account.</p>
          <div className="form-group">
            <input className="form-control" placeholder="Email" name="email" type="email" onChange={this.handleChange} value={this.state.email}></input>
          </div>
          <div className="form-group">
            <input className="form-control" placeholder="Password" name="password" onChange={this.handleChange} value={this.state.password} type="password"></input>
          </div>
          <div className="form-group">
            {this.state.error ? <p className="text-danger">{this.state.error}</p> : null}
            <button className="btn btn-primary rounded-pill px-5">Login</button>
          </div>
          <hr></hr>
          <p>Don't have an account? <Link to="/signup">Sign up</Link></p>
        </form>
      </div>
    )
  }
}

The only difference is that we import the signin method which is used to login the user.

Protected Component

We have already done the groundwork to redirect to this route once the user is authenticated. This makes the <Profile> component quite straightforward: 

// src/pages/Profile.js
import React, { Component } from 'react';
import Header from '../components/Header';
import Footer from '../components/Footer';
import { auth } from '../services/firebase';

export default class Profile extends Component {

  constructor() {
    super();
    this.state = {
      user: auth().currentUser,
    }
  }
  render() {
    return (
      <div>
        <Header></Header>
        <div>Login in as: <strong>{this.state.user.email}</strong></div>
        <Footer></Footer>
      </div>
    )
  }
}

It gets the details of the user and displays the email.

Our <Header> component also render different links depending on the authenticated state.

Our <Header> component also render different links depending on the authenticated state.

// src/component/Header.js
import React from 'react';
import { Link } from 'react-router-dom';
import { auth } from '../services/firebase';

function Header() {
  return (
    <header>
      <nav className="navbar navbar-expand-md navbar-light bg-light">
        <Link className="navbar-brand" to="/">ABC</Link>
        <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
          <span className="navbar-toggler-icon"></span>
        </button>
        <div className="collapse navbar-collapse justify-content-end" id="navbarNavAltMarkup">
          {auth().currentUser
            ? <div className="navbar-nav">
              <Link className="nav-item nav-link" to="/profile">Profile</Link>
              <button className="btn btn-primary" onClick={() => auth().signOut()}>Logout</button>
            </div>
            : <div className="navbar-nav">
              <Link className="nav-item nav-link" to="/login">Sign In</Link>
              <Link className="nav-item nav-link" to="/signup">Sign Up</Link>
            </div>}
        </div>
      </nav>
    </header>
  );
}

export default Header;

Conclusion

Authenticating users is one of the core functionalities of most web apps and getting it right quickly and securely is very important. Firebase saves you a lot of time and it works nicely with React.