Firebase v9 Authentication in a React App
User identity management is a cornerstone of application security, but building and maintaining custom authentication code is time-consuming and error-prone. Firebase's Authentication service provides ready-to-use functions that simplify registration, email verification, and login flows. This article walks through implementing these features in a React app using the modular Firebase v9 SDK.
To follow along, you'll need a Google account, Node installed, and familiarity with React hooks and Firebase v8. The source for the initial template is available in the companion GitHub repository linked in the original article.
Project and Authentication Setup
The first step is creating a Firebase project. From the Firebase console, click Add project, give it a name, and continue. No need to enable Google Analytics for this tutorial.


Next enable Email/Password authentication. Click the Authentication icon in the sidebar, select Get started, then choose the Email/Password provider and enable it.


Preparing the Starter Code
Clone the starter template from GitHub, which already includes Firebase v9 in its dependencies.
git clone -b starter https://github.com/Tammibriggs/Firebase_user_auth.git
cd Firebase_user_auth
npm install
Run npm install followed by npm start to launch the app.
Initializing Firebase in React
Add a web app to your Firebase project by clicking the web (</>) icon in the project overview. Register the app with a name to get your firebaseConfig object.



Copy the config into a new src/firebase.js file, then initialize Firebase and the Authentication service using the modular imports.
// src/firebase.js
import { initializeApp } from 'firebase/app'
import {getAuth} from 'firebase/auth'
// src/firebase.js
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
export {auth}
// src.firebase.js
import { initializeApp } from "firebase/app"
import { getAuth } from "firebase/auth"
const firebaseConfig = {
apiKey: "API_KEY",
authDomain: "AUTH_DOMAIN",
projectId: "PROJECT_ID",
storageBucket: "STORAGE_BUCKET",
messagingSenderId: "MESSAGING_SENDER_ID",
appId: "APP_ID"
}
// Initialize Firebase and Firebase Authentication
const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
export {auth}
Building User Registration
The createUserWithEmailAndPassword function is the means to register a user in v9. The auth instance is always the first argument, followed by the email and password.
The starter template's Register.js file includes the three form fields and controlled state. Add a validation function to check the password and confirmation values.
// src/Register.js
// ...
const validatePassword = () => {
let isValid = true
if (password !== '' && confirmPassword !== ''){
if (password !== confirmPassword) {
isValid = false
setError('Passwords does not match')
}
}
return isValid
}
// ...
The function returns an isValid boolean. This will gate the registration call. Add the Firebase and authentication imports:
// src/Register.js
import {auth} from './firebase'
import {createUserWithEmailAndPassword} from 'firebase/auth'
Now create a function that invokes createUserWithEmailAndPassword only when validation passes.
// src/Register.js
// ...
const register = e => {
e.preventDefault()
setError('')
if(validatePassword()) {
// Create a new user with email and password using firebase
createUserWithEmailAndPassword(auth, email, password)
.then((res) => {
console.log(res.user)
})
.catch(err => setError(err.message))
}
setEmail('')
setPassword('')
setConfirmPassword('')
}
// ...
Wire the function to the form's onSubmit event so the registration triggers when the form is submitted.
// src/Register.js
<form onSubmit={register} name='registration_form'>
Registering a user from http://localhost:3000/register will log the new user's details to the browser's console.

Managing User State with Context API
The Context API is a clean way to share the user state across the component tree without prop drilling. We then use the onAuthStateChanged observer to populate that state, which is the recommended way to get the current user, ensuring we avoid intermediate auth states.
Create a context plus two helper functions in a new file.
// src/AuthContext.js
import React, {useContext} from 'react'
const AuthContext = React.createContext()
export function AuthProvider({children, value}) {
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
export function useAuthValue(){
return useContext(AuthContext)
}
AuthProvider: shares the user's state to all children ofAuthContext.ProvideruseAuthValue: allows easy access to the provided value
In App.js, wrap the components with AuthProvider, passing the user's current value.
// src/App.js
// ...
import {useState} from 'react'
import {AuthProvider} from './AuthContext'
function App() {
const [currentUser, setCurrentUser] = useState(null)
return (
<Router>
<AuthProvider value={{currentUser}}>
<Switch>
...
</Switch>
</AuthProvider>
</Router>
);
}
export default App;
At this point, currentUser is always null until we set it. Import the necessary hooks and set the state that observes the authentication status.
// src/App.js
import {useState, useEffect} from 'react'
import {auth} from './firebase'
import {onAuthStateChanged} from 'firebase/auth'
// src/App.js
// ...
useEffect(() => {
onAuthStateChanged(auth, (user) => {
setCurrentUser(user)
})
}, [])
// ...
Verifying Registered Emails
To force email verification before granting access, use the sendEmailVerification function on the registered user's object. The code should send the email, then redirect to a verify-email page using useHistory.
// src/Register.js
import {useHistory, Link} from 'react-router-dom'
import {createUserWithEmailAndPassword, sendEmailVerification} from 'firebase/auth'
// src/Register.js
// ...
const history = useHistory()
// ...
// src/Register.js
// ...
.then(() => {
sendEmailVerification(auth.currentUser)
.then(() => {
history.push('/verify-email')
}).catch((err) => alert(err.message))
})
// ...

The verify-email page has multiple jobs: display the user's email, manage resending emails while preventing Firebase rate-limit errors, and route to the profile once a user verifies.
Import useAuthValue to pull the current user.
// src/VerifyEmail.js
import {useAuthValue} from './AuthContext'
// src/VerifyEmail.js
const {currentUser} = useAuthValue()
Display the email using optional chaining to make the code null-safe.
// src/VerifyEmail.js
// ...
<span>{currentUser?.email}</span>
// ...
Resending Emails and the Countdown Timer
Firebase imposes a 60-second interval between verification emails to the same address. Button state and UI should communicate this. Add the necessary imports and a state for the button's enabled status.
// src/VerifyEmail.js
import {useState} from 'react'
import {auth} from './firebase'
import {sendEmailVerification} from 'firebase/auth'
// src/VerifyEmail.js
const [buttonDisabled, setButtonDisabled] = useState(false)
Create the handler for resending the email.
// src/VerifyEmail.js
// ...
const resendEmailVerification = () => {
setButtonDisabled(true)
sendEmailVerification(auth.currentUser)
.then(() => {
setButtonDisabled(false)
}).catch((err) => {
alert(err.message)
setButtonDisabled(false)
})
}
// ...
Bind the handler to the button.
// ...
<button
onClick={resendEmailVerification}
disabled={buttonDisabled}
>Resend Email</button>
// ...
Instead of a manual button state, build a proper countdown. Add a useEffect import along with a time state for the seconds and a timeActive state to control the countdown start.
import {useState, useEffect} from 'react'
// src/VerifyEmail.js
const [time, setTime] = useState(60)
const [timeActive, setTimeActive] = useState(false)
// src/VerifyEmail.js
// ...
useEffect(() => {
let interval = null
if(timeActive && time !== 0 ){
interval = setInterval(() => {
setTime((time) => time - 1)
}, 1000)
}else if(time === 0){
setTimeActive(false)
setTime(60)
clearInterval(interval)
}
return () => clearInterval(interval);
}, [timeActive, time])
// ...
Start the countdown when the email is sent by setting timeActive in the sendEmailVerification handler.
// src/VerifyEmail.js
// ...
.then(() => {
setButtonDisabled(false)
setTimeActive(true)
})
// ...
Display the countdown inside the button and disable it while the countdown runs.
// src/VerifyEmail.js
<button
onClick={resendEmailVerification}
disabled={buttonDisabled}
>Resend Email {timeActive && time}</button>
disabled={timeActive}
This still leaves a case where a user lands on the verify-email page for the first time and clicks the resend button immediately, potentially triggering an error if Firebase hasn't been given 60 seconds. The solution is to lift the timeActive state to the Context API, so the state is globally controlled. Move the state to the App component.
// src/App.js
function App() {
// ...
const [timeActive, setTimeActive] = useState(false)
// ...
// src/App.js
// ...
<AuthProvider value={{currentUser, timeActive, setTimeActive}}>
// ...
De-structure both values in the VerifyEmail component.
// src/VerifyEmail.js
const {timeActive, setTimeActive} = useAuthValue()
In Register.js, start the timer right after the registration flow's initial email is sent.
// src/Register.js
import {useAuthValue} from './AuthContext'
// src/Register.js
const {setTimeActive} = useAuthValue()
// src/Register.js
// ...
.then(() => {
setTimeActive(true)
history.push('/verify-email')
})
// ...
Navigating After Verification
The final verification step uses the user object's reload function to listen for verification status changes. Every second, the code checks if the email has been verified, and once it is, the user is routed to the profile page.
// src/VerifyEmail.js
import {useHistory} from 'react-router-dom'
// src/VerifyEmail.js
const history = useHistory()
// src/VerifyEmail.js
// ...
useEffect(() => {
const interval = setInterval(() => {
currentUser?.reload()
.then(() => {
if(currentUser?.emailVerified){
clearInterval(interval)
history.push('/')
}
})
.catch((err) => {
alert(err.message)
})
}, 1000)
}, [history, currentUser])
// ...

Profile Display and Sign Out
The profile page will use the currentUser state from AuthContext.
// src/Profile.js
import './profile.css'
import {useAuthValue} from './AuthContext'
function Profile() {
const {currentUser} = useAuthValue()
return (
<div className='center'>
<div className='profile'>
<h1>Profile</h1>
<p><strong>Email: </strong>{currentUser?.email}</p>
<p>
<strong>Email verified: </strong>
{`${currentUser?.emailVerified}`}
</p>
<span>Sign Out</span>
</div>
</div>
)
}
export default Profile
For signing out, the signOut function takes the auth instance as its only argument.
// src/Profile.js
import { signOut } from 'firebase/auth'
import { auth } from './firebase'
// src/Profile.js
// ...
<span onClick={() => signOut(auth)}>Sign Out</span>
// ...
Protecting the Profile Route
Currently, any user can visit the profile route, verified or not. A PrivateRoute component checks the user object's emailVerified flag and redirects unverified users to the login page.
// src/PrivateRoute.js
import {Route, Redirect} from 'react-router-dom'
import {useAuthValue} from './AuthContext'
export default function PrivateRoute({component:Component, ...rest}) {
const {currentUser} = useAuthValue()
return (
<Route
{...rest}
render={props => {
return currentUser?.emailVerified ? <Component {...props} /> : <Redirect to='/login' />
}}>
</Route>
)
}
Apply it to the profile route by creating the PrivateRoute component and importing it into the router config.
// src/App.js
import PrivateRoute from './PrivateRoute'
// src/App.js
<PrivateRoute exact path="/" component={Profile} />
Creating Login Functionality
Login with signInWithEmailAndPassword also needs to evaluate email verification status, and send another verification email if the status is unverified before redirecting to the verify-email page.
Add the necessary imports and hook up the login function.
import {signInWithEmailAndPassword, sendEmailVerification} from 'firebase/auth'
import {auth} from './firebase'
import {useHistory} from 'react-router-dom'
import {useAuthValue} from './AuthContext'
// src/Login.js
const {setTimeActive} = useAuthValue()
const history = useHistory()
// src/Login.js
// ...
const login = e => {
e.preventDefault()
signInWithEmailAndPassword(auth, email, password)
.then(() => {
if(!auth.currentUser.emailVerified) {
sendEmailVerification(auth.currentUser)
.then(() => {
setTimeActive(true)
history.push('/verify-email')
})
.catch(err => alert(err.message))
}else{
history.push('/')
}
})
.catch(err => setError(err.message))
}
// ...
// src/Login.js
<form onSubmit={login} name='login_form'>
The combination of the v9 Firebase Authentication API and React Context gives you registration, verification, private routes, and login. Building this from scratch is far more complex, making the Firebase service a practical choice for robust user identity management. For more on the API, reference the Firebase documentation listed in the original article.



