React Authentication Tutorial – How to Set Up Auth with Firebase V9 and React Router V6
Hey everyone! In this tutorial, we‘ll walk through how to add authentication to a React app from scratch using the latest versions of Firebase (v9) and React Router (v6).
We‘ll cover:
- Setting up Login and Registration with Firebase Auth
- Storing auth tokens and protecting private routes
- Handling errors and improving UX
- Best practices for security and scalability
By the end, you‘ll have a fully functioning React app with email/password auth that you can extend however you like. Let‘s get started!
Project Setup
First, make sure you have Node.js installed. Open up your terminal and run:
npx create-react-app react-firebase-auth
cd react-firebase-auth
npm start
This will set up a new React project and start the development server at localhost:3000
.
Next, let‘s install the dependencies we‘ll need:
npm install firebase react-router-dom @mui/material @emotion/react @emotion/styled react-toastify
Here‘s what each one does:
firebase
– allows us to use Firebase services in our appreact-router-dom
– provides routing capabilities@mui/material
,@emotion/react
,@emotion/styled
– the Material UI library for pre-styled componentsreact-toastify
– lets us easily display toast messages
Creating Our Components
Inside the src
folder, create a new folder called components
. This is where we‘ll put our reusable components.
Let‘s make a Form.js
file inside it for our Login and Registration forms:
import { useState } from ‘react‘;
import { TextField, Button, Typography } from ‘@mui/material‘;
const Form = ({ title, handleClick }) => {
const [email, setEmail] = useState(‘‘);
const [pass, setPass] = useState(‘‘);
return (
<>
<Typography variant="h4">{title}</Typography>
<TextField
label="Email"
onChange={(e) => setEmail(e.target.value)}
margin="normal"
/>
<TextField
label="Password"
type="password"
onChange={(e) => setPass(e.target.value)}
margin="normal"
/>
<Button
variant="contained"
onClick={() => handleClick(email, pass)}
>
{title}
</Button>
</>
);
};
export default Form;
This component accepts a title
prop to specify whether it‘s for Login or Registration. It also takes a handleClick
prop which will be a function to execute on form submission.
We‘re using the useState
hook to manage the form inputs. Material UI‘s TextField
and Button
components give us some pre-built, good looking form elements.
Setting Up Firebase
Go to https://firebase.google.com/ and click "Get Started" to set up a new Firebase project. Give it a name and accept the terms to create the project.
Once that‘s done, click "Add App" and select "Web" to register your React app with Firebase.
This will give you a code snippet with your Firebase configuration. We‘ll store this in a separate file to keep things organized.
Make a new file called firebase.js
in the src
folder:
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app‘s Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyD2zZbTGcxiP4ue9l0ypL7Wz0KzMcx_bPU",
authDomain: "myapp-28eda.firebaseapp.com",
projectId: "myapp-28eda",
storageBucket: "myapp-28eda.appspot.com",
messagingSenderId: "374147814979",
appId: "1:374147814979:web:79c85a23e960b0b83cc4dd"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Initialize Firebase Authentication and get a reference to the service
export const auth = getAuth(app);
Make sure to replace the firebaseConfig
object with your own app‘s configuration.
We‘re initializing our Firebase app and exporting an auth
instance that we can use throughout our app for authentication.
Implementing Registration
Open up App.js
and import the necessary modules:
import { BrowserRouter as Router, Routes, Route } from ‘react-router-dom‘;
import { ToastContainer } from ‘react-toastify‘;
import ‘react-toastify/dist/ReactToastify.css‘;
import { createUserWithEmailAndPassword } from ‘firebase/auth‘;
import { auth } from ‘./firebase‘;
import Form from ‘./components/Form‘;
Then set up your routes:
function App() {
return (
<Router>
<Routes>
<Route
path="/register"
element={
<Form
title="Register"
handleClick={register}
/>
}
/>
</Routes>
<ToastContainer />
</Router>
);
}
The /register
route renders our Form
component with the title "Register". We‘re passing a register
function as the handleClick
prop.
Let‘s define that function above the App
component:
const register = async (email, password) => {
try {
const user = await createUserWithEmailAndPassword(
auth,
email,
password
);
console.log(user);
} catch (error) {
console.log(error.message);
}
};
Here we‘re using the createUserWithEmailAndPassword
function from Firebase Auth to register a new user with their email and password.
If registration is successful, we log the newly created user to the console. If it fails, we catch the error and log the error message.
Now if you visit localhost:3000/register
, you should see a basic registration form. Try submitting it with an email and password, and check the console to confirm the user was created.
Adding Login
The process for login is very similar. Update your routes to include a /login
route:
<Routes>
{/* ... other routes */}
<Route
path="/login"
element={
<Form
title="Login"
handleClick={login}
/>
}
/>
</Routes>
And define the login
function:
const login = async (email, password) => {
try {
const user = await signInWithEmailAndPassword(auth, email, password);
console.log(user);
} catch (error) {
console.log(error.message);
}
};
This uses the signInWithEmailAndPassword
function to authenticate an existing user. Again, we‘re just logging the authenticated user or error message to the console for now.
Test out the login form to make sure it‘s working. Use the email and password of the user you registered earlier.
Handling Errors
Instead of just logging errors to the console, let‘s display them to the user.
We can use the react-toastify
library to easily show toast messages.
Modify the login
and register
functions:
import { toast } from ‘react-toastify‘;
// ...
const login = async (email, password) => {
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (error) {
toast.error(error.message);
}
};
const register = async (email, password) => {
try {
await createUserWithEmailAndPassword(auth, email, password);
} catch (error) {
toast.error(error.message);
}
};
Now if there‘s an error on login or registration, a toast message will display it to the user.
Some common errors to handle:
- "auth/invalid-email" – check that the user has entered a valid email format
- "auth/wrong-password" – let the user know the password was incorrect
- "auth/weak-password" – require a stronger password
- "auth/email-already-in-use" – the email is already registered, redirect to login instead
Storing Auth State
We need a way to keep track of whether the user is currently logged in as they navigate through the app.
We can use the onAuthStateChanged
observer from Firebase Auth, along with React‘s useState
hook:
import { useState, useEffect } from ‘react‘;
import { onAuthStateChanged } from ‘firebase/auth‘;
function App() {
const [user, setUser] = useState({});
useEffect(() => {
onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
});
}, [])
return (
{/* ... */}
);
}
The onAuthStateChanged
observer listens for changes in the user‘s authentication state. When they log in or out, it will set the user
state accordingly.
We can then use the user
state to conditionally render parts of our UI. For example, we might show a logout button if user
exists, otherwise we show the login/register options.
Protecting Routes
In most apps, you‘ll want to protect certain routes so that only authenticated users can access them. We can achieve this with a custom ProtectedRoute
component:
import { Navigate } from ‘react-router-dom‘;
const ProtectedRoute = ({ user, children }) => {
if (!user) {
return <Navigate to="/login" replace />;
}
return children;
};
This component checks if there is a user
prop passed to it. If not, that means there is no authenticated user, so it redirects to the /login
page using React Router‘s Navigate
component.
Otherwise, if there is a user
, it renders its children
– the protected content.
You can use it like this in your routes:
<Route
path="/dashboard"
element={
<ProtectedRoute user={user}>
<Dashboard />
</ProtectedRoute>
}
/>
Now your /dashboard
route (or any other protected routes) will only be accessible to logged in users. Trying to access it without being authenticated will redirect you to login.
Adding Logout
Finally, let‘s add the ability for users to log out. In Firebase terms, this is called "signing out".
Create a logout button component:
import { useNavigate } from ‘react-router-dom‘;
import { signOut } from ‘firebase/auth‘;
import { auth } from ‘../firebase‘;
const LogoutButton = () => {
const navigate = useNavigate();
const handleLogout = () => {
signOut(auth).then(() => {
navigate("/login");
console.log("Signed out successfully")
}).catch((error) => {
console.log(error.message)
});
};
return (
<Button onClick={handleLogout}>
Logout
</Button>
);
};
export default LogoutButton;
Clicking this button calls the signOut
function from Firebase Auth. When that resolves successfully, we use React Router‘s useNavigate
hook to send the user back to the /login
page.
You can conditionally render the logout button based on the user
state:
{user ? <LogoutButton /> : <LoginButton />}
Handling Loading State
Since authentication actions like login, logout and checking auth state are asynchronous, there will likely be times when your app is waiting for them to complete.
It‘s a good idea to handle this "in between" state to avoid flickers in the UI or accessing properties on an undefined user object.
One approach is to have a dedicated isLoading
state variable:
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
onAuthStateChanged(auth, (user) => {
setUser(user);
setIsLoading(false);
});
}, []);
if (isLoading) return <LoadingSpinner />
Initially isLoading
is set to true
. When the onAuthStateChanged
observer resolves, we update the user and also set isLoading
to false
.
While isLoading
is true, we can render a loading spinner or placeholder component to let the user know something is happening. Once it becomes false, we render the actual authenticated app UI.
You can also set isLoading
back to true
temporarily during login/logout actions.
Next Steps
Congrats, you now have a fully functioning React app with Firebase authentication! Some additional things to consider:
- Persisting auth state so users stay logged in on refresh
- Adding sign in methods like Google, Facebook, etc.
- Implementing password reset/forgot password flow
- Storing and displaying user profile data
The great thing about Firebase Auth is that it‘s incredibly flexible and extensible. You can easily mix and match authentication methods, customize the UI to fit your app, and integrate with other Firebase services like Firestore and Cloud Functions.
I hope this tutorial helped you get up and running with Firebase Auth in your React apps. Let me know if you have any questions!