Firebase authentication with React

Hunter Becton

Hunter Becton

Introduction

When it comes to authentication you have two options: create the backend yourself or use a service like Firebase, Auth0, Okta, or another hosted auth service.

Creating a backend to handle authentication requires more development time and the risk that you miss some critical security features. That’s why many developers turn to Firebase for their authentication. They can get setup quickly, have plenty of options for customization, and don’t have to worry about all the added security gotchas that come with custom backends. Plus, Firebase offers a generous free tier and fair pricing that scales with your application.

In this guide you will learn how to add authentication to a React application using Firebase. Not only will you learn how easy it is to add Firebase to your application, but you will also learn about:

  • Custom React hooks
  • Forms and validation
  • ReCAPTCHA
  • Toaster notifications
  • React Router

Project repo

The code for this project is open source and available on GitHub. I use Gumroad for those that want to be generous and donate, but no purchase is required. 😊

Grab the code

Project demo

Give it a whirl!

Getting started

Before you get started creating your application there are a few items you need to ensure you have installed and configured on your local machine.

Code editor

In this guide I will be using VSCode, but feel free to use your favorite code editor. However, there are some extensions that this guide will use that might not be available on other code editors. Also, you will need to set up the code shortcut on Mac in order to open VSCode from the terminal.

Extensions

In this guide I will be using the following VSCode Extensions (you can install these directly from VSCode):

Terminal

In this guide I will be using iTerm, but feel free to use your favorite terminal or the one that comes installed on Windows or Mac.

Node and NPM

You will need to install Node and NPM on your local machine before starting this guide. Follow these steps to install Node on your operating system.

Git

Git is a version control system for tracking changes in source code during development. You’ll need to follow this guide to install Git on your operating machine.


React

Create React App

Open your terminal and navigate to where you want to create your project on your computer. In my case, I’ll change directories by running cd Documents/_skillthrive/_guides in the terminal.

If you followed the getting started section you should have Node installed on your computer. Once installed you can run npx create-react-app my-app in your terminal to create a new React project, replacing my-app with the name of your application.

Now change directories in your terminal by running cd my-app (replacing my-app with your actual project) and running npm i to install the Node Modules. Once those are installed you can open the project in VSCode by running code . in your terminal.

You can open your terminal inside VSCode with the command CTRL + ` and run the following command to start your React project: npm run start. Now you should have your React application running on localhost:3000.

Cleanup boilerplate

Project root

You’ve already installed your Node Modules using NPM, but you could use another package manager like Yarn. Because you aren’t using Yarn in this tutorial you can go ahead and remove the yarn.lock file.

Although you may want to add a README to your final project, you’re not going to use the one that comes with Create React App, so go ahead and delete theREADME.md file as well.

Src folder

In this guide you will be using styled components instead of standard CSS files, so go ahead and remove the following files from your project: App.css and index.css. After removing those CSS files you’ll need to remove the imports of them in your App.js and index.js files.

You will not be running any tests in this guide, so remove the following files: App.test.js, reportWebVitals.js, and setupTest.js. Remove the import of reportWebVitals and the function call in your index.js file. With web-vitals not being used you can remove the dependency in your package.json file.

Last, remove the logo.svg file and the code where it uses the logo in your App.js file. Now your project should be free from any extra code it doesn’t need.


Firebase

Create a Firebase project

Head to Firebase and sign up for an account. Then head to the console and create a new project. Enter your project name and click continue. The next screen will ask if you want to install Google Analytics. For now, deselect this option and click continue. Give Firebase a few moments to build your project.

Create a new Firebase project from the console

Once your project is set up you’ll want to add Firebase to your application. You can do this from the Project Overview page by clicking on the Web icon. On the next screen enter your application name and click Register app. After your application is registered you will be presented with a couple values you’ll need for initializing your Firebase application with React.

You’ll save these values in a .env.local file in your React application. You can read more about adding custom environment variables in the Create React App documentation. Create the .env.local file and add the following to the file, replacing the values in quotes with the ones Firebase assigns your application:

REACT_APP_FB_API="your-apiKey"
REACT_APP_FB_DOMAIN="your-authDomain"
REACT_APP_FB_PROJECT="your-projectId"
REACT_APP_FB_BUCKET="your-storageBucket"
REACT_APP_FB_SENDER="your-messagingSenderId"
REACT_APP_FB_APP="your-appId"

If you reviewed the Create React App documentation above you would have read that you need to start all your environment variables with REACT_APP_. By sticking to this naming convention Create React App will handle everything for getting these variables in your code where you used them.

Once you’re done adding your environment variables click on Go to console.

Firebase authentication

The last thing you need to set up in Firebase is authentication. From the console click on Authentication and then Get started.

Click on the authentication tab in the Firebase console

The next step is to select the sign-in providers you want to support in your application. In this guide you’re going to use Email/Password, so select that option by clicking the edit icon.

Click the edit icon to enable email and password login for Firebase

Toggle on Enable and click Save. The last step is to change the default reset password email from Firebase. To do this click on the Templates tab, Password reset, and click the edit icon.

Click the edit icon to change the Password Reset email in Firebase

The first change you’ll make is to the Message. You’re not going to change much except to make the link for the password simpler by changing the anchor tag text to Reset password. Copy and paste the following into your message field:

<p>Hello,</p>
<p>Follow this link to reset your %APP_NAME% password for your %EMAIL% account.</p>
<p><a href='%LINK%'>Reset password</a></p>
<p>If you didn’t ask to reset your password, you can ignore this email.</p>
<p>Thanks,</p>
<p>Your %APP_NAME% team</p>

Last, click on the link towards the bottom that says Customize action URL and change the url to http://localhost:3000/reset-password. By default Firebase will send your users to a hosted page to change their password, but in this guide you’re going to set up a custom reset password form.

useAuth hook

In this section you’ll create a custom React hook that will create a user object and authentication methods that you can easily use throughout our application. You’ll also use React Context to create a provider that you can wrap around our application so components further down the component tree can access them when you call the custom hook.

To work with Firebase in React you’ll need to install the Firebase package from NPM. To install it run npm i firebase in your terminal.

Create a new folder in src called hooks and inside create a new file named useAuth.js. Add the following code to the file in order to import and initialize Firebase:

import React, { useState, useEffect, useContext, createContext } from 'react';
import firebase from 'firebase/app';
import 'firebase/auth';
 
// Initialize Firebase
firebase.initializeApp({
 apiKey: process.env.REACT_APP_FB_API,
 authDomain: process.env.REACT_APP_FB_DOMAIN,
 projectId: process.env.REACT_APP_FB_PROJECT,
 storageBucket: process.env.REACT_APP_FB_BUCKET,
 messagingSenderId: process.env.REACT_APP_FB_SENDER,
 appID: process.env.REACT_APP_FB_APP,
});

Notice that you’re using the environment variables stored in the .env.local file here. Because you’re using the REACT_APP_ naming convention there’s no more configuration needed to give this custom hook access to these values.

The auth hook was updated March, 3, 2021

Auth context

If you’re not familiar with Context in React be sure to read more about Context in the React documentation. In summary, Context provides a way to pass data through the component tree without having to pass props down manually at every level.

You’ll be using it in this application so any component that calls useAuth() will have the auth object available. Add on the following code to your useAuth.js file:

const AuthContext = createContext();
 
// Hook for child components to get the auth object ...
// ... and re-render when it changes.
export const useAuth = () => {
 return useContext(AuthContext);
};

First, you create context using the createContext() method form React. This context will be used when creating a Provider later in the code. The provider is what makes the context available to your components when you call useAuth().

The last bit of code you wrote in this section is to create and export the useAuth hook. All this is going to do is call the useContext hook from React, passing is the AuthContext you created above. As the useContext name implies, React will now be able to use the AuthContext and get access to the user object and authentication methods when useAuth() is called.

useProvideAuth

The next bit of code is where you write the useProvideAuth() function, which holds the user object, authentication methods, and tracks the user’s authentication status.

Add the following code to your useAuth.js file:

// Provider hook that creates auth object and handles state
export const AuthProvider = ({ children }) => {
 const [user, setUser] = useState(null);
 const [isAuthenticating, setIsAuthenticating] = useState(true);
 
 // Wrap any Firebase methods we want to use making sure ...
 // ... to save the user to state.
 const login = (email, password) => {
   return firebase
     .auth()
     .signInWithEmailAndPassword(email, password)
     .then((response) => {
       setUser(response.user);
       return response.user;
     });
 };
 
 const signup = (email, password) => {
   return firebase
     .auth()
     .createUserWithEmailAndPassword(email, password)
     .then((response) => {
       setUser(response.user);
       return response.user;
     });
 };
 
 const logout = () => {
   return firebase
     .auth()
     .signOut()
     .then(() => {
       setUser(false);
     });
 };
 
 const sendPasswordResetEmail = (email) => {
   return firebase
     .auth()
     .sendPasswordResetEmail(email)
     .then(() => {
       return true;
     });
 };
 
 const confirmPasswordReset = (code, password) => {
   return firebase
     .auth()
     .confirmPasswordReset(code, password)
     .then(() => {
       return true;
     });
 };
 
 // Subscribe to user on mount
 // Because this sets state in the callback it will cause any ...
 // ... component that utilizes this hook to re-render with the ...
 // ... latest auth object.
 useEffect(() => {
   const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
     setUser(user);
     setIsAuthenticating(false);
   });
 
   // Cleanup subscription on unmount
   return () => unsubscribe();
 }, []);
 
 // The user object and auth methods
 const values = {
   user,
   isAuthenticating,
   login,
   signup,
   logout,
   sendPasswordResetEmail,
   confirmPasswordReset,
 };
 
 // Provider component that wraps your app and makes auth object
 // ... available to any child component that calls useAuth().
 return (
   <AuthContext.Provider value={values}>
     {!isAuthenticating && children}
   </AuthContext.Provider>
 );
};
User state

The first step in this function is to create a state for the user object by calling the useState() React hook. The user value is the state and setUser() is the function that is called to update the state.

isAuthenticating state

The next state value tracks if the user is currently being authenticated. This is important for private routes because the user will be null when first visiting, so the user will be redirected to the login page even if they’re logged in. By checking !isAuthenticating you can conditionally render the children in the provider (more on that below).

login method

The login method takes two arguments: email and password. This function uses a method on firebase.auth() called signInWithEmailAndPassword. This method returns a promise, which is used to set and return the user object with response.user.

signup method

The signup method takes two arguments: email and password. This function uses a method on firebase.auth() called createUserWithEmailAndPassword. This method returns a promise, which is used to return the user object with response.user.

logout method

The logout method takes no arguments. This function uses a method on firebase.auth() called logout. This method returns a promise and sets the user object to false.

sendPasswordResetEmail method

The sendPasswordResetEmail method takes one argument: email. This function uses a method on firebase.auth() called sendPasswordResetEmail. This method returns a promise and simply returns true when resolved.

confirmPasswordReset method

The confirmPasswordReset method takes two arguments: code and password. This function uses a method on firebase.auth() called confirmPasswordReset. This method returns a promise and simply returns true when resolved.

useEffect hook

Whenever useAuth() is called this hook subscribes to the user and watches for any changes using the onAuthStateChanged method from Firebase. Because this sets state in the callback it will cause any component that utilizes this hook to re-render with the latest auth object. TheuseEffect() will also set isAuthenticating to false once the user object comes back from Firebase.

Return auth provider

The function will return an auth provider, which will be wrapped around the app to provide context to the state values and methods. A provider is created on the AuthContext and passed in values, which are an object of the state values and methods you want access to when calling useAuth().

The provider checks to see if !isAuthenticated to avoid logged in users being automatically redirected to the login page on a page refresh. Only then will the children (what the provider wraps) be rendered.

Auth provider

With the custom useAuth() hook complete you now need to wrap your application with the auth provider so components further down the component tree can access it. To do this open your index.js file and wrap your application like so:

import React from 'react';
import ReactDOM from 'react-dom';
 
import { AuthProvider } from './hooks/useAuth';
import App from './App';
 
ReactDOM.render(
 <React.StrictMode>
   <AuthProvider>
       <App />
   </AuthProvider>
 </React.StrictMode>,
 document.getElementById('root')
);

Theming

In this guide you’ll be using styled components to create styles and reusable components. There are many reasons you may want to use styled components in your React applications, including some of my favorite like scoped styles and flexible custom components.

To install styled components, open your terminal and run npm i styled-components.

Theme variables

A theme file holds variables that you will reuse throughout your application. This is handy because you can access these values via props in your components and update the values in one place instead of across your entire application.

Create a theme.js file in your styles folder and write the following code:

const theme = {
 fonts: {
   main: 'Poppins, sans-serif',
 },
 colors: {
   green1: '#30BC72',
   white1: '#CCCCCC',
   white2: '#F8F8F8',
   white3: '#FBFAFA',
   black1: '#1A1E21',
   black2: '#333C42',
   black3: '#51595E',
   red1: '#F6406C',
 },
 breakpoints: {
   xxs: 'only screen and (max-width: 30rem)',
   xs: 'only screen and (max-width: 40rem)',
   s: 'only screen and (max-width: 50rem)',
   m: 'only screen and (max-width: 70rem)',
 },
 animations: {
   link: 'color 0.3s ease',
   buttonGlow: 'box-shadow 0.3s ease',
 },
 shadows: {
   glowHoverGreenStart: '0px 4px 30px rgba(80, 209, 141, 0);',
   glowHoverGreenEnd: '0px 4px 30px rgba(80, 209, 141, 0.6);',
 },
};
 
export default theme;

Styled components make it easy to pass your theme values in props with a ThemeProvider. Open your index.js file and wrap the ThemeProvider around your app by writing the following code, passing in your imported Theme into the theme prop:

import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from 'styled-components';
 
import { ProvideAuth } from './hooks/useAuth';
import App from './App';
import Theme from './styles/theme';
 
ReactDOM.render(
 <React.StrictMode>
   <ProvideAuth>
     <ThemeProvider theme={Theme}>
       <App />
     </ThemeProvider>
   </ProvideAuth>
 </React.StrictMode>,
 document.getElementById('root')
);

Global styles

It’s common to include global styles in your application for things like CSS resets and fonts. Luckily, styled components make it easy to do global styles as well with createGlobalStyle.

Create a styles folder in src called GlobalStyles.js and add the following styles:

import { createGlobalStyle } from 'styled-components';
 
export const GlobalStyles = createGlobalStyle`
   * {
       margin: 0;
       padding: 0;
       box-sizing: border-box;
   }
 
   :root {
     font-size: 16px;
   }
  
   body {
     background-color: ${(props) => props.theme.colors.black1};
     font-family: 'Poppins', sans-serif;
     font-weight: 400;
   }
 
   ::selection {
     background: ${(props) => props.theme.colors.green1};
   }
 
   @media ${(props) => props.theme.breakpoints.m} {
   :root {
     font-size: 14px;
   }
 }
 
`;

This is your first look at how you can pass props into your styled components by accessing props.theme. Remember that these props are available to you because of the ThemeProvider. It’s also important to note that props are first passed in and used to return a value in your theme.

In order for your application to know about the global styles you’ll need to pass them in after your ThemeProvider. Open the index.js file and add the global styles to the code:

import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from 'styled-components';
 
import { ProvideAuth } from './hooks/useAuth';
import App from './App';
import { GlobalStyles } from './styles/GlobalStyles';
import Theme from './styles/theme';
 
ReactDOM.render(
 <React.StrictMode>
   <ProvideAuth>
     <ThemeProvider theme={Theme}>
       <GlobalStyles />
       <App />
     </ThemeProvider>
   </ProvideAuth>
 </React.StrictMode>,
 document.getElementById('root')
);

Google Fonts

Google Fonts are an open source collection of fonts that you can use in your application. Once you find a font you like you can select the styles you need.

Select the fonts styles you want from Google Fonts

Once all the styles are selected a new menu will appear on the right where you can copy and paste the code for including the fonts in your project.

Copy the link tag to import the Google fonts into your React application

For this guide you’ll be using the Poppins fonts, which you can include in the head of your index.html file, found in your public folder:

<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">

Layout

Grid component

A lot of design systems will utilize a grid component to build their components around. In this guide you’ll learn how to create your own grid component using styled components.

Create a components folder and another folder inside named Layout. Inside this folder create a file named Grid.jsx and write the following code:

import styled from 'styled-components';
 
const Grid = styled.div`
 display: grid;
 grid-template-columns: 1fr repeat(12, minmax(auto, 4.2rem)) 1fr;
 gap: ${(props) => (props.gapDefault ? props.gapDefault : '2rem')};
 margin: ${(props) => (props.marginDefault ? props.marginDefault : 0)};
 grid-template-rows: max-content;
 
 @media ${(props) => props.theme.breakpoints.m} {
   grid-template-columns: 2rem repeat(6, 1fr) 2rem;
   gap: ${(props) => (props.gapMedium ? props.gapMedium : '1rem')};
   margin: ${(props) => (props.marginMedium ? props.marginMedium : 0)};
 }
 
 @media ${(props) => props.theme.breakpoints.s} {
   grid-template-columns: 1rem repeat(6, 1fr) 1rem;
   gap: ${(props) => (props.gapSmall ? props.gapSmall : '1rem')};
   margin: ${(props) => (props.marginSmall ? props.marginSmall : 0)};
 }
`;
 
export default Grid;

The Grid component uses CSS Grid in order to create the layout it needs. To declare grid, you first set display: grid;. Next, you define the grid columns with the grid-template-columns property. The first value, 1fr, is a fractional unit. This unit is flexible and is calculated based on the space available.

For example, you could create a flexible, equal sized two column grid by setting grid-template-columns: 1fr 1fr;. But let’s say you want to make the second row twice as big as the first. You can easily do that by setting grid-template-columns: 1fr 2fr;. Now CSS grid will calculate the size of both columns and make the second one twice as big as the first.

In the Grid component you’re using 1fr on each side of the center 12 columns. These values will be calculated for you and will act as white space, which you typically see in max-width layouts.

The middle 12 columns use minmax() to calculate a minimum and maximum size for the grid. The minimum size will be set to auto so CSS grid can calculate that for us. The maximum size will be set to 4.2rem, meaning that a column cannot exceed this size. This will make it so our content will never extend past a max width.

The gap property sets spacing around the columns and rows. This property checks to see if you passed in a gapDefault prop and sets it to a default value if it doesn’t exist.

Similarly, the margin property checks to see if you passed in a marginDefault props and sets it to a default value if it doesn’t exist.

Next, you set the height for the rows by setting grid-template-rows: max-content. This means the rows will be as big as the biggest content item.

Last, you wrote your media queries for smaller screens. Essentially the concepts are the same, but instead of using 1fr on the white space you’re setting it to 2rem on medium screens and 1rem on small screens. Also, the columns in the middle switch to 6 instead of 12 and they’re calculated using fractional units.

The Nav component will be using a Link and NavLink component from React Router, which is the routing solution you’ll be using in this guide. You’ll need to install React Router by running npm i react-router-dom in your terminal.

The NavLink is a special version of the Link component that can be used to add styling to a link if it matches the current route. You can read more about that in the React Router documentation.

Create a new folder in your components folder called Nav and create a new file inside called Nav.jsx. You’ll also be using your first image for the logo, so create another folder inside src called images and import your logo. Write the following code:

import React from 'react';
import styled from 'styled-components';
import { NavLink, Link } from 'react-router-dom';
 
import Logo from '../../images/logo.svg';
import { useAuth } from '../../hooks/useAuth';
 
const Nav = () => {
 const auth = useAuth();
 
 return (
   <NavContainer>
     <Link to='/'>
       <img src={Logo} alt='Logo' />
     </Link>
     {auth.user && <button onClick={() => auth.logout()}>Logout</button>}
     {!auth.user && (
       <>
         <NavLink to='/signup'>Sign Up</NavLink>
         <NavLink to='/login'>Login</NavLink>
       </>
     )}
   </NavContainer>
 );
};
 
const NavContainer = styled.nav`
 grid-column: 2 / span 12;
 padding: 1rem 0;
 display: flex;
 align-items: center;
 justify-content: flex-end;
 
 img {
   width: 3.125rem;
 }
 
 button {
   border: none;
   background-color: transparent;
   cursor: pointer;
 }
 
 a:first-child {
   margin-right: auto;
 }
 
 a,
 button {
   font-size: 1rem;
   color: ${(props) => props.theme.colors.white3};
   text-decoration: none;
   margin-left: 2rem;
   transition: ${(props) => props.theme.animations.link};
 }
 
 a:hover,
 a:focus,
 button:hover,
 button:focus {
   color: ${(props) => props.theme.colors.green1};
 }
 
 @media ${(props) => props.theme.breakpoints.m} {
   grid-column: 2 / span 6;
 }
`;
 
export default Nav;

This is the first component that uses the useAuth() hook. By setting the hook to a constant called auth you can read the user on auth.user. This can be used to conditionally render a login, signup, and logout button depending on if the user is logged in or not. Also, the logout button will call auth.logout when clicked.

The styles here are straightforward, but there are a couple things to point out. The navigation elements will be aligned using Flexbox. By setting display: flex; and justify-content: flex-end; the navigation elements will be aligned to the rightmost edge of the navigation. However, this is overridden for the logo by setting margin-right: auto;. This will make the logo aligned to the leftmost edge of the navigation.

Also notice how you can target the links inside the navigation with just the anchor tag and without any extra class name. This is because styled components are scoped to each component.

Layout component

The Layout component is going to wrap your entire application. This prevents you from having to import Nav multiple times. Create a new component in the Layout folder named Layout.jsx and write the following code:

import React from 'react';
import styled from 'styled-components';
 
import Grid from './Grid';
import Nav from '../Nav/Nav';
 
const Layout = ({ children }) => {
 return (
   <>
     <LayoutGrid>
       <Nav />
       {children}
     </LayoutGrid>
   </>
 );
};
 
const LayoutGrid = styled(Grid)`
 min-height: 100vh;
`;
 
export default Layout;

Notice the LayoutGrid styled component. So far in the guide you created styled components from HTML elements, but the library also makes it possible to add styles to a component that already exists. What this means is that the LayoutGrid will have the same styles as Grid, but you can add or override those styles as you need. In this component you wanted the Layout to fill the entire height of the screen, so you set min-height: 100vh;.

Button component

The Button component will be a styled component that you’ll use in the next section when creating the ReCAPTCHA button. Create a new folder in components called Button and inside create a new file named Button.jsx. Write the following code inside:

import styled from 'styled-components';
 
const Button = styled.button`
 padding: 0.5rem 1.5rem;
 margin: ${(props) => (props.margin ? props.margin : 0)};
 color: ${(props) => props.theme.colors.white3};
 font-size: 1rem;
 line-height: 1.25rem;
 letter-spacing: normal;
 background-color: ${(props) =>
   props.backgroundColor ? props.backgroundColor : props.theme.colors.green1};
 border: none;
 border-radius: 1.25rem;
 box-shadow: ${(props) =>
   props.boxShadow
     ? props.boxShadow
     : props.theme.shadows.glowHoverGreenStart};
 transition: ${(props) => props.theme.animations.buttonGlow};
 cursor: ${(props) => (props.cursor ? props.cursor : 'pointer')};
 width: fit-content;
 
 &:hover,
 &:focus {
   box-shadow: ${(props) =>
     props.hover ? props.hover : props.theme.shadows.glowHoverGreenEnd};
 }
`;
 
export default Button;

Again, the styles here are straightforward. However, take note that you’re adding some flexibility to this component by looking for props like backgroundColor, boxShadow and hover. These can be used to override the default values.


Typography

H1 component

The first typography component you’ll create is for styled h1 tags. Create a new folder in components named Typography and create a new file named H1.jsx. Inside write the following code:

import styled from 'styled-components';
 
const H1 = styled.h1`
 color: ${(props) => props.theme.colors.white3};
 font-weight: 600;
 font-size: 4rem;
 line-height: 5.25rem;
 margin: ${(props) => (props.margin ? props.margin : '0')};
 text-align: ${(props) => (props.textAlign ? props.textAlign : 'left')};
 
 @media ${(props) => props.theme.breakpoints.s} {
   font-size: 3.25rem;
   line-height: 3.75rem;
 }
`;
 
export default H1;

Text component

The next component is a styled component for text in your application. This component is a great example of how flexible styled component can be because you’ll be using switch statements inside the component to apply different styles. Create a new file in the Typography folder named Text.jsx and write the following:

import styled from 'styled-components';
 
const Text = styled.p`
 color: ${(props) => (props.color ? props.color : props.theme.colors.white3)};
 font-size: ${(props) => {
   switch (props.size) {
     case 'small':
       return '1rem';
     case 'medium':
       return '1.125rem';
     case 'large':
       return '1.5rem';
     default:
       return '1.125rem';
   }
 }};
 font-weight: ${(props) => {
   switch (props.weight) {
     case 'normal':
       return '400';
     case 'bold':
       return '600';
     default:
       return '400';
   }
 }};
 line-height: ${(props) => {
   switch (props.size) {
     case 'small':
       return '1.25rem';
     case 'medium':
       return '1.5rem';
     case 'large':
       return '1.75rem';
     default:
       return '1.25rem';
   }
 }};
 margin: ${(props) => (props.margin ? props.margin : '0')};
 text-align: ${(props) => (props.textAlign ? props.textAlign : 'left')};
`;
 
export default Text;

In this component you can see how you can utilize switch statements inside styled components to create flexible styles. For example, the size prop can be passed into this component and used inside CSS properties like font-size and line-height to dynamically set values.


ReCAPTCHA

More than likely you’ve seen the ReCAPTCHA challenges when trying to log into a website.

Create a new Firebase project from the console

These challenges are designed to stop bots from signing up and flooding your website. However, with ReCAPTCHA v3 you can now help prevent bots without asking users to complete a challenge. Version 3 tracks the user’s behavior and a score for each request. If the score doesn’t pass then the person or bot trying to gain access cannot complete login.

Register a new site

Visit the Google ReCAPTCHA console and create a new site by clicking on the plus icon:

Register a new site for Google ReCAPTCHA

Fill out the form, selecting reCAPTCHA v3 and adding localhost as a domain.

Select ReCAPTCHA V3 when registering your site with Google

Copy the values from the next page in your env.local file under the variables REACT_APP_CAPTCHA_SITE and REACT_APP_CAPTCHA_SECRET.

ReCAPTCHA component

Create a new folder in components named Auth and create a new file named ReCaptcha.jsx.This component will use a new package, so install it by running npm i react-google-recaptcha-v3. Write the following code:

import React from 'react';
import {
 GoogleReCaptchaProvider,
 useGoogleReCaptcha,
} from 'react-google-recaptcha-v3';
 
import Button from '../Button/Button';
 
const CaptchaButton = ({
 onVerifyCaptcha,
 backgroundColor,
 hover,
 verified,
 ...rest
}) => {
 const { executeRecaptcha } = useGoogleReCaptcha();
 
 const clickHandler = async () => {
   if (!executeRecaptcha) {
     return;
   }
 
   const token = await executeRecaptcha('contact');
 
   onVerifyCaptcha(token);
 };
 
 return (
   <Button
     onClick={clickHandler}
     type='button'
     backgroundColor={backgroundColor}
     cursor={verified ? 'default' : 'pointer'}
     hover={hover}
     disabled={verified ? true : false}
     {...rest}
   >
     {verified ? 'Verified' : 'Verify you are human'}
   </Button>
 );
};
 
export const ReCaptcha = ({
 onVerifyCaptcha,
 backgroundColor,
 hover,
 verified,
 ...rest
}) => (
 <GoogleReCaptchaProvider reCaptchaKey={process.env.REACT_APP_CAPTCHA_SITE}>
   <CaptchaButton
     onVerifyCaptcha={onVerifyCaptcha}
     backgroundColor={backgroundColor}
     hover={hover}
     verified={verified}
     {...rest}
   />
 </GoogleReCaptchaProvider>
);

The first component is a Button component that when clicked will create a new token from Google ReCAPTCHA if the user is verified. A few custom props are passed in to adjust the styling of the button. The verified prop will be passed in later when the token state is tracked with React Hook Form. The state of this variable will be used to adjust styling, disable and enable the button, and conditionally render the button’s text.

The ReCaptcha component is what’s actually exported from this file and it’s wrapped with the GoogleReCaptchaProvider from the react-google-recaptcha-v3 package. This provider takes the REACT_APP_CAPTCHA_SITE environment variable which is used when verifying the request and generating a token.


Toaster

A toaster is an user interface element, usually a small box with a message on the edge of a website, that’s used to communicate something to the user. You’ll be using a toaster to let the user know the status of their authentication requests. To make this easier you’ll be using a package called React Toastify, so run npm i react-toastify to install it.

StyledToaster component

The toaster comes with some styles out-of-the-box, but you can easily change them by targeting certain class names, which you can find here. To change the styles to match the application, you’ll first need to create a new folder in components called Toast and inside create a new component called StyledToast.jsx.

This component will use styled components to change the ToastContainercomponent, which is provided to you by the react-toastify package. Inside the file write the following code:

import styled from 'styled-components';
import { ToastContainer } from 'react-toastify';
 
const StyledToast = styled(ToastContainer)`
 .Toastify__toast {
   border-radius: 0;
 }
 
 .Toastify__toast--default {
   background-color: ${(props) => props.theme.colors.black2};
   color: ${(props) => props.theme.colors.white3};
 }
 
 .Toastify__close-button--default {
   color: ${(props) => props.theme.colors.white3};
   opacity: 1;
 }
 
 .Toastify__progress-bar--default {
   background: ${(props) => props.theme.colors.green1};
 }
 
 .Toastify__toast--error {
   background-color: ${(props) => props.theme.colors.red1};
 }
`;
 
export default StyledToast;

With the toaster styled to match the application, you now need to add it along with the rest of the toaster’s default CSS to your index.js file like so:

import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from 'styled-components';
import 'react-toastify/dist/ReactToastify.css';
 
import { ProvideAuth } from './hooks/useAuth';
import App from './App';
import { GlobalStyles } from './styles/GlobalStyles';
import Theme from './styles/theme';
import StyledToast from './components/Toast/StyledToast';
 
ReactDOM.render(
 <React.StrictMode>
   <ProvideAuth>
     <ThemeProvider theme={Theme}>
       <GlobalStyles />
       <StyledToast />
       <App />
     </ThemeProvider>
   </ProvideAuth>
 </React.StrictMode>,
 document.getElementById('root')
);

Dashboard

Dashboard component

The Dashboard component is a simple component that is only going to be used to demonstrate a private route. It’s also going to get access to the user object from the useAuth() hook and display the email of the current logged in user. Create a new folder in components named Dashboard and create a new file inside named Dashboard.jsx. Inside the file write the following code:

import React from 'react';
import styled from 'styled-components';
 
import H1 from '../Typography/H1';
import Text from '../Typography/Text';
import { useAuth } from '../../hooks/useAuth';
 
const Dashboard = () => {
 const auth = useAuth();
 
 return (
   <DashboardContainer>
     <H1>Dashboard</H1>
     <Text>Logged in as {auth.user.email}</Text>
   </DashboardContainer>
 );
};
 
const DashboardContainer = styled.div`
 grid-column: 2 / span 12;
 padding: 3rem 0 5.5rem 0;
 display: flex;
 flex-direction: column;
 align-items: center;
 
 @media ${(props) => props.theme.breakpoints.m} {
   grid-column: 2 / span 6;
 }
`;
 
export default Dashboard;

Routing

Routing is going to allow you to navigate your application based on paths. You’ll be using React Router to handle routing, so install it by running npm i react-router-dom.

React router

Your router will be in your App.js file, so head there and write the following code:

import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
 
import Layout from './components/Layout/Layout';
import Dashboard from './components/Dashboard/Dashboard';
 
const App = () => {
 return (
   <Router>
     <Layout>
       <Switch>
         <Route exact path='/'>
           <Dashboard />
         </Route>
       </Switch>
     </Layout>
   </Router>
 );
};
 
export default App;

All of your routes need to be inside a BrowserRouter (imported as Router) component. The next component is your Layout component, which wraps all your because each component will use the grid and navigation.

The next component in line is the Switch component. The Switch component will still have nested Route components that need exact paths. The added functionality of Switch is that it will only render the first matched Route child. This is handy when you have nested routes like those in your App.js file.

The exact prop is passed into the Dashboard route because without it the Dashboard would render on every route that has / in it. Last, you pass the component you want to render on the route, which is the Dashboard component.

Now if you boot your application and try to go to the homepage your application will run into an issue: user is undefined. This is when the PrivateRoute component comes into the picture.

PrivateRoute component

Anyone not authenticated should be redirected to login when trying to view the Dashboard component on the homepage. To enable this you’ll write a PrivateRoute component. Create a new folder in components named Route and a new file inside named PrivateRoute.jsx. Write the following code:

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
 
import { useAuth } from '../../hooks/useAuth';
 
const PrivateRoute = ({ children, ...rest }) => {
 const auth = useAuth();
 
 return (
   <Route
     {...rest}
     render={() => (auth.user ? children : <Redirect to='/login' />)}
   ></Route>
 );
};
 
export default PrivateRoute;

This component calls the useAuth() hook, thus getting access to the user object if it exists. This component then returns a Route component that uses the render function to handle what component to render. If a user exists, then whatever component wrapped inside the PrivateRoute will be rendered. Otherwise, the user will be redirected to the login page using the Redirect component from React Router.


Auth forms

Now it’s time to add the forms to the application. In this section of the guide you will use a couple of packages to make it easy to work with forms and validate the data. Install them by running npm i react-hook-form yup @hookform/resolvers.

The form library is called React Hook Form, and it’s a simple, efficient way to work with forms in React. Yup makes it easy to parse and validate data, which will be important for checking emails and passwords. The resolver package makes it possible for the React Hook Form and Yup to work together.

FormContainer component

The form container is a styled div that will wrap every form. Start by creating a new file named FormContainer.jsx inside the Auth folder and write the following code:

import styled from 'styled-components';
 
const FormContainer = styled.div`
 grid-column: 5 / span 6;
 padding: 3rem 0 5.5rem 0;
 display: flex;
 flex-direction: column;
 align-items: center;
 text-align: center;
 
 a {
   font-size: 1rem;
   color: ${(props) => props.theme.colors.white3};
   text-decoration: none;
   transition: ${(props) => props.theme.animations.link};
 }
 
 a:hover,
 a:focus {
   color: ${(props) => props.theme.colors.green1};
 }
 
 @media ${(props) => props.theme.breakpoints.m} {
   grid-column: 3 / span 4;
 }
 
 @media ${(props) => props.theme.breakpoints.xxs} {
   grid-column: 2 / span 6;
 }
`;
 
export default FormContainer;

Form component

The form component is a styled form that will be used for each form. Create a new file named Form.jsx in your Auth folder and write the following code:

import styled from 'styled-components';
 
const Form = styled.form`
 display: flex;
 flex-direction: column;
 align-items: center;
 width: 100%;
 
 input {
   box-sizing: border-box;
   margin: 0 0 1rem 0;
   background-color: ${(props) => props.theme.colors.black2};
   color: ${(props) => props.theme.colors.white2};
   font-family: ${(props) => props.theme.fonts.main};
   font-size: 1rem;
   border: none;
   padding: 0.5rem 1rem;
 }
 
 input:focus {
   outline: 0.15rem solid ${(props) => props.theme.colors.green1};
 }
 
 input::placeholder {
   color: ${(props) => props.theme.colors.white3};
 }
 
 input[type='email'],
 input[type='password'] {
   width: 60%;
 }
 
 input[type='submit'] {
   padding: 0.5rem 1.5rem;
   margin: 0 0 1rem 0;
   color: ${(props) => props.theme.colors.white3};
   font-size: 1rem;
   line-height: 1.25rem;
   letter-spacing: normal;
   background-color: ${(props) => props.theme.colors.green1};
   border: none;
   border-radius: 1.25rem;
   box-shadow: ${(props) => props.theme.shadows.glowHoverGreenStart};
   transition: ${(props) => props.theme.animations.buttonGlow};
   cursor: pointer;
 }
 
 input[type='submit']:hover,
 input[type='submit']:focus {
   box-shadow: ${(props) => props.theme.shadows.glowHoverGreenEnd};
 }
 
 @media ${(props) => props.theme.breakpoints.s} {
   input[type='email'],
   input[type='password'] {
     width: 80%;
   }
 }
`;
 
export default Form;

Signup component

The next couple of components are going to use React Hook Form and Yup. They’re the most complex components in this guide, but once you have the hang of one the rest are using the same principles. With this in mind, reference the explanation below the Signup component for a detailed explanation for the common concepts in all the components. Specific details for each component will be added in each section if needed.

Create a new component named Signup.jsx and write the following code:

import React, { useEffect, useRef } from 'react';
import { useTheme } from 'styled-components';
import { useForm } from 'react-hook-form';
import * as Yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { toast } from 'react-toastify';
import { useHistory } from 'react-router-dom';
 
import H1 from '../Typography/H1';
import FormContainer from './FormContainer';
import Text from '../Typography/Text';
import ReCaptcha from './ReCaptcha';
import { useAuth } from '../../hooks/useAuth';
import Form from './Form';
 
const validationSchema = Yup.object().shape({
 email: Yup.string()
   .required('Email is required')
   .matches(
     /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/,
     'Invalid email'
   ),
 password: Yup.string()
   .required('Password is required')
   .matches(
     /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/,
     'Must contain 8 characters, one uppercase, one lowercase, one number and one special case character'
   ),
 passwordConfirm: Yup.string().oneOf(
   [Yup.ref('password'), null],
   'Passwords must match'
 ),
 captchaToken: Yup.string().required('Verify you are a human'),
});
 
const Signup = () => {
 const {
   register,
   handleSubmit,
   errors,
   reset,
   setValue,
   clearErrors,
   watch,
 } = useForm({
   resolver: yupResolver(validationSchema),
 });
 
 const auth = useAuth();
 
 const theme = useTheme();
 
 const history = useHistory();
 
 const submitRef = useRef(null);
 
 const emailRef = useRef(null);
 
 const passwordRef = useRef(null);
 
 const passwordConfirmRef = useRef(null);
 
 // Manually register captchaToken
 useEffect(() => {
   register({ name: 'captchaToken' });
 },[]);
 
 // Watch for changes to captcha
 const watchCaptcha = watch('captchaToken');
 
 // Set focus on email
 useEffect(() => {
   emailRef.current.focus();
 }, []);
 
 const onSubmit = async (data) => {
   try {
     await auth.signup(data.email, data.password);
     toast('Welcome! 👋');
 	reset();
     history.push('/');
   } catch {
     toast.error('Error signing up.');
   }
 };
 
 const onVerifyCaptcha = (token) => {
   setValue('captchaToken', token);
   clearErrors(['captchaToken']);
   submitRef.current.focus();
 };
 
 return (
   <FormContainer>
     <H1 textAlign='center' margin='0 0 2rem 0'>
       Sign Up
     </H1>
     <Text margin='0 0 1rem 0' textAlign='center'>
       Enter an email and password.
     </Text>
     <Form onSubmit={handleSubmit(onSubmit)}>
       <input
         type='email'
         name='email'
         placeholder='Email'
         autoComplete='off'
         ref={(e) => {
           register(e);
           emailRef.current = e;
         }}
       />
       {errors.email && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.email.message}
         </Text>
       )}
       <input
         type='password'
         name='password'
         placeholder='Password'
         ref={(e) => {
           register(e);
           passwordRef.current = e;
         }}
       />
       {errors.password && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.password.message}
         </Text>
       )}
       <input
         type='password'
         name='passwordConfirm'
         placeholder='Confirm Password'
         ref={(e) => {
           register(e);
           passwordConfirmRef.current = e;
         }}
       />
       {errors.passwordConfirm && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.passwordConfirm.message}
         </Text>
       )}
       <ReCaptcha
         onVerifyCaptcha={onVerifyCaptcha}
         backgroundColor={
           watchCaptcha ? theme.colors.black1 : theme.colors.black2
         }
         hover={'none'}
         verified={watchCaptcha}
         margin='0 0 1rem 0'
       />
       {errors.captchaToken && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.captchaToken.message}
         </Text>
       )}
       <input type='submit' value='Submit' ref={submitRef} />
     </Form>
   </FormContainer>
 );
};
 
export default Signup;

Validation schema

After importing everything you need for this component, the next step is to define the validation schema using Yup. You define a schema object by calling the object().shape() and setting a key and value for each input that needs validation. Most of the time the key should be the same as the name value for each input. In this case, the names of the inputs are password and passwordConfirm.

However, there’s another value you want to validate, which is the ReCAPTCHA token. The ReCAPTCHA token isn’t an input field, so you need to manually register it using the register() function from React Hook Form. This is done in an useEffect() hook. Once the value is registered you can update the value using the setValue() function from React Hook Form. This function is called inside the onVerifyCaptcha() function.

As for the values inside the validation object, you’ll use more methods from Yup to define the data type and run validations. For example, email is defined as a string by calling Yup.string(). You go even further by chaining on the require() method, which requires that the field not be empty. You also pass in an error message in case this validation fails.

Last, you chain on the matches() method, which takes a regular expression and checks it against the value. Turn to Google to find regular expressions from other developers that work for your use case. This method also has an error message argument in case the validation fails.

The last validation method used is on the password confirm field. The method is called oneOf(), and it takes an array of possible values as an argument. In this case, the possible values can be the current value of the password field or null. The last argument to the method is the error message you want to appear if validation fails.

These are the only three methods you’ll be using in this guide, but you can explore the Yup API documentation for more.

Refs and hooks

Under the validation schema object is where you created the Signup component. The first thing you did inside the component is get all the methods you’ll be using from React Hook Form by calling the useForm() hook.

This hook also takes an argument for custom resolvers, which is where you call yupResolver() from @hookform/resolvers/yup and pass in the custom validation schema object you created above.

You’ll also see in the React Hook Form docs that this is where you can define the default values of the fields. When doing this approach, you can easily create references to the input fields by calling the register() method on the input’s ref attribute. However, in this guide you’re taking a different approach that’s going to allow you to share the ref usage, which is going to make it possible to focus on the email field when the component first mounts.

Below the useForm() hook you called more hooks, including useAuth(), useTheme(), and useHistory(). The useAuth() hook will give you access to the user object and auth methods you wrote earlier in the guide.

The useTheme() hook comes from styled components and gives you access to the theme object, which you’ll use to change the style of the ReCaptcha component depending on if it’s verified.

The useHistory() hook comes from React Router and will make it easy to programmatically navigate to other pages once the form is submitted.

The next hook you called is React’s useRef() hook. This is used to create a reference to the input fields, and the initial value of these references are set to null. You’ll use these references on each input’s ref attribute to register the value with React Hook Form and the useRef() hook.

The last hook you’re going to use is React’s useEffect() hook. The first useEffect() hook will manually register the captchaToken with React Hook Form. Once that’s registered you can use the watch() method from React Hook Form to create a constant that’s always set to the captchaToken value. This constant will be used inside the verified prop in the ReCaptcha component to track if the user has verified themselves. Once they have been verified the background color will change to match the body background.

The second useEffect() hook is used to focus on the email input when the component mounts. You do this by targeting the email reference, getting the current reference (emailRef.current), and then chaining on the focus() method. This isn’t necessary, but it’s a nice addition because the user can start typing their email instead of selecting the input when they first visit the page.

onSubmit

The onSubmit() function is what’s passed into our form’s onSubmit() event, then passed into React Hook Form’s handleSubmit() function. By passing onSubmit() into handleSubmit() your function will get access to the data object, which holds all the values from your form.

React Hook Form will check that all the validations have passed before the form can be submitted. If there is an error you can display it in the DOM using the errors that you imported from React Hook Form (more on that below).

The onSubmit() function is asynchronous because it awaits a response from Firebase. Because it’s an async function, you should wrap the code in a try / catch block.

Inside the try block you will call the signup() function on the auth object that you imported from your custom useAuth() hook. This function requires an email and password to be provided, which React Hook Form makes easily accessible on the data object.

If everything goes well the next step is for your application to send a friendly welcome message using toast() and then navigate to the homepage by using history.push('/');. If an error occurs the catch block will use toast.error() to show an error message.

onVerifyCaptcha

The onVerifyCaptcha() function gets access to the ReCAPTCHA token from the clickHandler() function in your ReCaptcha component. This token is then used to update the captchaToken value using the setValue() method from React Hook Form.

Next, the function clears any errors that might be stored in React Hook Form by using the clearErrors() method. Last, the focus changes to the submit button so the user can easily submit the form without having to click on the button.

Component

The rendered component is wrapped with the FormContainer component and starts off with the H1 and Text component for a title and description of the form.

Below that is the Form component which needs the onSubmit() event handler. Inside this event handler you call the handleSubmit() function from React Hook Form and pass in the custom onSubmit() function you wrote above. This will allow your custom function to get access to the data object that it needs to send over the email and password to Firebase.

In the form is the first input for email. It’s important that you pass in the type, name, placeholder, and ref attributes. The autoComplete attribute is optional, but you can omit it if you want autocomplete to be active.

With React Hook Form you would typically see the register() method passed into the ref attribute like so ref={register}, but instead you called register() in a way that will allow you to share ref usage. This will allow you to focus on the email input when the component mounts by using useEffect() because email.current will be defined.

Under the email input you will see errors used from React Hook Form. This will conditionally render red text if there’s anything on the errors.email object.

This is continued for the rest of the inputs, changing values where necessary for the matching input. Toward the bottom of the form you will see the ReCaptcha component, which uses the watchCaptcha value to conditionally style the component.

Login component

The Login component is almost identical to the Signup component, but with slight tweaks. One addition is the use of the Link component from React Router. This creates a link that can be used to navigate to the forgot password form.

Please refer to the Signup component for more information about the code inside. For now, create a new file named Login.jsx and write the following code:

import React, { useEffect, useRef } from 'react';
import { useTheme } from 'styled-components';
import { useForm } from 'react-hook-form';
import * as Yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { toast } from 'react-toastify';
import { useHistory, Link } from 'react-router-dom';
 
import H1 from '../Typography/H1';
import Text from '../Typography/Text';
import FormContainer from './FormContainer';
import { ReCaptcha } from './ReCaptcha';
import { useAuth } from '../../hooks/useAuth';
import Form from './Form';
 
const validationSchema = Yup.object().shape({
 email: Yup.string()
   .required('Email is required')
   .matches(
     /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/,
     'Invalid email'
   ),
 password: Yup.string().required('Password is required'),
 captchaToken: Yup.string().required('Verify you are a human'),
});
 
const Login = () => {
 const {
   register,
   handleSubmit,
   errors,
   reset,
   setValue,
   clearErrors,
   watch,
 } = useForm({
   resolver: yupResolver(validationSchema),
 });
 
 const auth = useAuth();
 
 const theme = useTheme();
 
 const history = useHistory();
 
 const submitRef = useRef(null);
 
 const emailRef = useRef(null);
 
 const passwordRef = useRef(null);
 
 // Manually register captchaToken
 useEffect(() => {
   register({ name: 'captchaToken' });
 },[]);
 
 // Watch for changes to captcha
 const watchCaptcha = watch('captchaToken');
 
 // Set focus on email
 useEffect(() => {
   emailRef.current.focus();
 }, []);
 
 const onSubmit = async (data) => {
   try {
     await auth.login(data.email, data.password);
     toast('Welcome! 👋');
  	reset();
     history.push('/');
   } catch {
     toast.error('Error logging in.');
   }
 };
 
 const onVerifyCaptcha = (token) => {
   setValue('captchaToken', token);
   clearErrors(['captchaToken']);
   submitRef.current.focus();
 };
 
 return (
   <FormContainer>
     <H1 textAlign='center' margin='0 0 2rem 0'>
       Login
     </H1>
     <Text margin='0 0 1rem 0' textAlign='center'>
       Enter your email and password.
     </Text>
     <Form onSubmit={handleSubmit(onSubmit)}>
       <input
         type='email'
         name='email'
         placeholder='Email'
         autoComplete='off'
         ref={(e) => {
           register(e);
           emailRef.current = e;
         }}
       />
       {errors.email && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.email.message}
         </Text>
       )}
       <input
         type='password'
         name='password'
         placeholder='Password'
         ref={(e) => {
           register(e);
           passwordRef.current = e;
         }}
       />
       {errors.password && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.password.message}
         </Text>
       )}
       <ReCaptcha
         onVerifyCaptcha={onVerifyCaptcha}
         backgroundColor={
           watchCaptcha ? theme.colors.black1 : theme.colors.black2
         }
         hover={'none'}
         verified={watchCaptcha}
         margin='0 0 1rem 0'
       />
       {errors.captchaToken && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.captchaToken.message}
         </Text>
       )}
       <input type='submit' value='Submit' ref={submitRef} />
     </Form>
     <Link to='/forgot-password'>Forgot password?</Link>
   </FormContainer>
 );
};
 
export default Login;

ForgotPassword component

The ForgotPassword component is almost identical to the Signup component, but with slight tweaks. Please refer to the Signup component for more information about the code inside. For now, create a new file named ForgotPassword.jsx and write the following code:

import React, { useEffect, useRef } from 'react';
import { useTheme } from 'styled-components';
import { useForm } from 'react-hook-form';
import * as Yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { toast } from 'react-toastify';
 
import Text from '../Typography/Text';
import H1 from '../Typography/H1';
import FormContainer from './FormContainer';
import { ReCaptcha } from './ReCaptcha';
import { useAuth } from '../../hooks/useAuth';
import Form from './Form';
 
const validationSchema = Yup.object().shape({
 email: Yup.string()
   .required('Email is required')
   .matches(
     /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/,
     'Invalid email'
   ),
 captchaToken: Yup.string().required('Verify you are a human'),
});
 
const ForgotPassword = () => {
 const {
   register,
   handleSubmit,
   errors,
   reset,
   setValue,
   clearErrors,
   watch,
 } = useForm({
   resolver: yupResolver(validationSchema),
 });
 
 const auth = useAuth();
 
 const theme = useTheme();
 
 const submitRef = useRef(null);
 
 const emailRef = useRef(null);
 
 // Manually register captchaToken
 useEffect(() => {
   register({ name: 'captchaToken' });
 },[]);

 // Watch for changes to captcha
 const watchCaptcha = watch('captchaToken');
 
 // Set focus on email
 useEffect(() => {
   emailRef.current.focus();
 }, []);
 
 const onSubmit = async (data) => {
   try {
     await auth.sendPasswordResetEmail(data.email);
     toast('Check email to complete.');
 	reset();
   } catch {
     toast.error('Error resetting password.');
   }
 };
 
 const onVerifyCaptcha = (token) => {
   setValue('captchaToken', token);
   clearErrors(['captchaToken']);
   submitRef.current.focus();
 };
 
 return (
   <FormContainer>
     <H1 textAlign='center' margin='0 0 2rem 0'>
       Forgot Password
     </H1>
     <Text margin='0 0 1rem 0' textAlign='center'>
       Enter your email.
     </Text>
     <Form onSubmit={handleSubmit(onSubmit)}>
       <input
         type='email'
         name='email'
         placeholder='Email'
         autoComplete='off'
         ref={(e) => {
           register(e);
           emailRef.current = e;
         }}
       />
       {errors.email && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.email.message}
         </Text>
       )}
       <ReCaptcha
         onVerifyCaptcha={onVerifyCaptcha}
         backgroundColor={
           watchCaptcha ? theme.colors.black1 : theme.colors.black2
         }
         hover={'none'}
         verified={watchCaptcha}
         margin='0 0 1rem 0'
       />
       {errors.captchaToken && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.captchaToken.message}
         </Text>
       )}
       <input type='submit' value='Submit' ref={submitRef} />
     </Form>
   </FormContainer>
 );
};
 
export default ForgotPassword;

ResetPassword component

Again, the ResetPassword component is very similar to the Signup component, but there are some additions that need to be pointed out.

First, this component uses the useLocation() hook from React Router. This hook is needed because when a user clicks on their email to reset their password the URL will include parameters. One of these parameters is a unique token that needs to be sent to Firebase in order to reset the password.

You can easily parse the parameters by using the Query String package, so install it by running npm i query-string. Once it’s installed you can create an object of all the parameters inside the onSubmit() function by writing const parsed = queryString.parse(location.search). Once you have that you can get the oobCode that Firebase needs on parsed.oobCode.

Please refer to the Signup component for more information about the code inside. For now, create a new file named ResetPassword.jsx and write the following code:

import React, { useEffect, useRef } from 'react';
import { useTheme } from 'styled-components';
import { useForm } from 'react-hook-form';
import * as Yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { toast } from 'react-toastify';
import { useHistory, useLocation } from 'react-router-dom';
import queryString from 'query-string';
 
import H1 from '../Typography/H1';
import FormContainer from './FormContainer';
import Text from '../Typography/Text';
import { ReCaptcha } from './ReCaptcha';
import { useAuth } from '../../hooks/useAuth';
import Form from './Form';
 
const validationSchema = Yup.object().shape({
 password: Yup.string()
   .required('Password is required')
   .matches(
   /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/,
     'Must contain 8 characters, one uppercase, one lowercase, one number and one special case character'
   ),
 passwordConfirm: Yup.string().oneOf(
   [Yup.ref('password'), null],
   'Passwords must match'
 ),
 captchaToken: Yup.string().required('Verify you are a human'),
});
 
const ResetPassword = () => {
 const {
   register,
   handleSubmit,
   errors,
   reset,
   setValue,
   clearErrors,
   watch,
 } = useForm({
   resolver: yupResolver(validationSchema),
 });
 
 const auth = useAuth();
 
 const theme = useTheme();
 
 const history = useHistory();
 
 const location = useLocation();
 
 const submitRef = useRef(null);
 
 const passwordRef = useRef(null);
 
 const passwordConfirmRef = useRef(null);
 
 // Manually register captchaToken
 useEffect(() => {
   register({ name: 'captchaToken' });
 },[]);
 
 // Watch for changes to captcha
 const watchCaptcha = watch('captchaToken');
 
 // Set focus on password
 useEffect(() => {
   passwordRef.current.focus();
 }, []);
 
 const onSubmit = async (data) => {
   try {
     const parsed = queryString.parse(location.search);
     await auth.confirmPasswordReset(parsed.oobCode, data.password);
     toast('Password reset.');
 	reset();
     history.push('/login');
   } catch {
     toast.error('Error resetting password.');
   }
 };
 
 const onVerifyCaptcha = (token) => {
   setValue('captchaToken', token);
   clearErrors(['captchaToken']);
   submitRef.current.focus();
 };
 
 return (
   <FormContainer>
     <H1 textAlign='center' margin='0 0 2rem 0'>
       Reset Password
     </H1>
     <Text margin='0 0 1rem 0' textAlign='center'>
       Enter new password.
     </Text>
     <Form onSubmit={handleSubmit(onSubmit)}>
       <input
         type='password'
         name='password'
         placeholder='Password'
         ref={(e) => {
           register(e);
           passwordRef.current = e;
         }}
       />
       {errors.password && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.password.message}
         </Text>
       )}
       <input
         type='password'
         name='passwordConfirm'
         placeholder='Confirm Password'
         ref={(e) => {
           register(e);
           passwordConfirmRef.current = e;
         }}
       />
       {errors.passwordConfirm && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.passwordConfirm.message}
         </Text>
       )}
       <ReCaptcha
         onVerifyCaptcha={onVerifyCaptcha}
         backgroundColor={
           watchCaptcha ? theme.colors.black1 : theme.colors.black2
         }
         hover={'none'}
         verified={watchCaptcha}
         margin='0 0 1rem 0'
       />
       {errors.captchaToken && (
         <Text
           color='#F6406C'
           size='small'
           margin='0 0 1rem 0'
           textAlign='center'
         >
           {errors.captchaToken.message}
         </Text>
       )}
       <input type='submit' value='Submit' ref={submitRef} />
     </Form>
   </FormContainer>
 );
};
 
export default ResetPassword;

Handling 404 routes

The NotFound component will be displayed on any route that isn’t defined in our router. In other words, it will act as your application’s 404 page. You’ll pass this component as the last Route in your router. The Route will not have a path prop defined, so the Switch component will use it as the fallback.

NotFound component

In your components folder create a new folder named NotFound. Inside create a new file named NotFound.jsx and write the following code:

import React from 'react';
import styled from 'styled-components';
 
import H1 from '../Typography/H1';
 
const NotFound = () => {
 return (
   <NotFoundContainer>
     <H1>404</H1>
   </NotFoundContainer>
 );
};
 
const NotFoundContainer = styled.div`
 grid-column: 2 / span 12;
 padding: 3rem 0 5.5rem 0;
 display: flex;
 flex-direction: column;
 align-items: center;
 
 @media ${(props) => props.theme.breakpoints.m} {
   grid-column: 2 / span 6;
 }
`;
 
export default NotFound;

Production

Your application is almost ready for production! Before you get started pushing your code to a server, you need to finish the routing. Open your App.js file and include the following:

import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
 
import Layout from './components/Layout/Layout';
import Signup from './components/Auth/Signup';
import Login from './components/Auth/Login';
import ForgotPassword from './components/Auth/ForgotPassword';
import ResetPassword from './components/Auth/ResetPassword';
import Dashboard from './components/Dashboard/Dashboard';
import PrivateRoute from './components/Route/PrivateRoute';
import NotFound from './components/NotFound/NotFound';
 
const App = () => {
 return (
   <Router>
     <Layout>
       <Switch>
         <PrivateRoute exact path='/'>
           <Dashboard />
         </PrivateRoute>
         <Route path='/signup'>
           <Signup />
         </Route>
         <Route path='/login'>
           <Login />
         </Route>
         <Route path='/forgot-password'>
           <ForgotPassword />
         </Route>
         <Route path='/reset-password'>
           <ResetPassword />
         </Route>
         <Route>
           <NotFound />
         </Route>
       </Switch>
     </Layout>
   </Router>
 );
};
 
export default App;

GitHub

Before pushing your code to GitHub you need to make sure that you have Git installed on your machine, so follow this guide if you need to do that.

Once you have Git installed, it’s now time to host your code to Github. Head to GitHub and create a new repository. Then back in your project open your terminal and write the following commands, replacing your-username and your-repo with the appropriate values:

git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:your-username/your-repo.git
git push -u origin main

Refresh your GitHub repository and you should see your code now being hosted on GitHub!

Vercel

In this guide we’re going to host our application on Vercel, but feel free to use any host you want. To get started, sign up for a Vercel account if you haven’t already.

Once signed in click on New Project.

Create a new project in Vercel

You will be prompted to connect Vercel to your GitHub account if you haven’t already. Once connected you can select your repo from the dropdown or by searching for it.

Import your project from GitHub into a new Vercel project

Next, select whether this project will live in a team account or personal account. On the next page leave the input fields as is, unless you want to rename the project. Then toggle the Environment Variables accordion to add your variables and values from your .local.env file.

Add your environment variables into Vercel

Once you have your environment variables added click on Deploy and Vercel will begin building your application. Once the deployment is complete you will get a URL for your application. Copy this URL and head back to your Google ReCAPTCHA console and replace localhost with your new domain from Vercel. Without this your ReCAPTCHA will not work because the domain is not authorized.

Firebase and ReCAPTCHA

The last step is to head back to your Firebase console and replace http://localhost:3000 in your reset password template with the URL you got from Vercel.

Change your custom URL in Firebase with the project link you got from Vercel