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. 😊
Project demo
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):
- Babel JavaScript
- REST Client
- Prettier Code Formatter
- ESLint
- Bracket Pair Colorizer
- Material Theme (Palenight)
- Material Icon Theme
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.
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.
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.
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.
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.
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.
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.
Nav component
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.
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:
Fill out the form, selecting reCAPTCHA v3 and adding localhost
as a domain.
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 ToastContainer
component, 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.
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.
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.
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.