Building a Firebase Authentication and Private Route System in a React App
Firebase Authentication is a robust and secure solution to handle user authentication in web applications. It offers various authentication methods, including email and password, Google Sign-In, and more. In this article, we'll walk through building a Firebase Authentication system in a React application. We'll provide you with the necessary code snippets and guide you through the process.
Prerequisites
Before we get started, make sure you have the following:
Node.js and npm installed on your machine.
A Firebase project. You can create one by visiting the Firebase Console.
Firebase SDK installed in your project. You can add it using
npm install firebase
.
Setting Up Firebase
First, initialize Firebase in your project by creating a Firebase configuration file. Replace the placeholders with your Firebase project details but make sure to keep your API keys secure.
// FirebaseConfig.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
export default auth;
Creating the Authentication Provider
Next, let's create an AuthProvider
component that will provide authentication-related functionality to our app.
// AuthProvider.js
import {
createUserWithEmailAndPassword,
onAuthStateChanged,
signInWithEmailAndPassword,
signOut,
} from "firebase/auth";
import { createContext, useEffect, useState } from "react";
import PropTypes from "prop-types";
import auth from "./FirebaseConfig";
export const AuthContext = createContext(null);
const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const createUser = (email, password) => {
setLoading(true);
return createUserWithEmailAndPassword(auth, email, password);
};
const loginUser = (email, password) => {
setLoading(true);
return signInWithEmailAndPassword(auth, email, password);
};
const logOut = () => {
setLoading(true);
return signOut(auth);
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
const authValue = {
createUser,
user,
loginUser,
logOut,
loading,
};
return <AuthContext.Provider value={authValue}>{children}</AuthContext.Provider>;
};
AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default AuthProvider;
Building the User Interface
Now that we have our authentication logic set up, let's create the user interface components. We'll create components for login, signup, profile, and a private route.
Header Component
The Header component is responsible for rendering the navigation bar at the top of the application. It handles the display of navigation links, user authentication status, and provides a logout button for authenticated users.
import { useContext } from "react";
import { Link, NavLink, useNavigate } from "react-router-dom";
import { AuthContext } from "./AuthProvider";
const Header = () => {
// Access the user, logOut, and loading state from the AuthContext
const { user, logOut, loading } = useContext(AuthContext);
// Use the useNavigate hook to programmatically navigate between pages
const navigate = useNavigate();
// Handle user logout
const handleSignOut = () => {
logOut()
.then(() => {
console.log("User logged out successfully");
navigate("/login"); // Redirect to the login page after logout
})
.catch((error) => console.error(error));
};
// Define navigation links based on user authentication status
const navLinks = (
<>
<li>
<NavLink to="/">Home</NavLink>
</li>
{!user && (
<>
<li>
<NavLink to="/login">Login</NavLink>
</li>
<li>
<NavLink to="/sign-up">Sign-Up</NavLink>
</li>
</>
)}
</>
);
// Render loading indicator if authentication state is still loading
return loading ? (
<span className="loading loading-dots loading-lg flex item-center mx-auto"></span>
) : (
<div>
{/* Render the navigation bar */}
<div className="navbar bg-base-100">
<div className="navbar-start">
{/* Dropdown menu for mobile devices */}
<div className="dropdown">
<label tabIndex={0} className="btn btn-ghost lg:hidden">
{/* Hamburger icon */}
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
</label>
{/* Dropdown content with navigation links */}
<ul
tabIndex={0}
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
{navLinks}
</ul>
</div>
{/* Application title */}
<a className="btn btn-ghost normal-case text-xl">Firebase Auth</a>
</div>
<div className="navbar-center hidden lg:flex">
{/* Horizontal navigation menu for larger screens */}
<ul className="menu menu-horizontal px-1">{navLinks}</ul>
</div>
<div className="navbar-end">
{/* Display user information and logout button if authenticated */}
{user && <a className="btn">{user.displayName}</a>}
{user && (
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
{/* User profile picture */}
<div className="w-10 rounded-full">
<img src="/images/stock/photo-1534528741775-53994a69daeb.jpg" />
</div>
</label>
{/* Dropdown content for user profile */}
<ul
tabIndex={0}
className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
<li>
<Link to="/profile">
{/* Profile link */}
<span className="justify-between">Profile</span>
</Link>
</li>
<li>
<a onClick={handleSignOut}>Logout</a>
{/* Logout button */}
</li>
</ul>
</div>
)}
</div>
</div>
</div>
);
};
export default Header;
Explanation:
The Header component uses the
useContext
hook to access theuser
,logOut
, andloading
state from theAuthContext
. This allows it to display the user's information and handle authentication actions.It uses the
useNavigate
hook fromreact-router-dom
to navigate to different pages in the application.The
handleSignOut
function is responsible for logging the user out. It calls thelogOut
function from the context and, upon success, redirects the user to the login page.The
navLinks
variable defines the navigation links based on the user's authentication status. If the user is not authenticated, it includes links to the login and sign-up pages.The component conditionally renders a loading indicator if the
loading
state istrue
.The navigation bar is styled using CSS classes provided by the application's design framework (e.g.,
navbar
,btn
,menu
, etc.).
Login Component
The Login component provides a form for users to log in to their accounts.
import { useContext } from "react";
import { AuthContext } from "./AuthProvider";
import { useNavigate } from "react-router-dom";
const Login = () => {
const { loginUser, loading, user } = useContext(AuthContext);
const navigate = useNavigate();
// If authentication is still loading, display a loading indicator
if (loading) {
return (
<span className="loading loading-dots loading-lg flex item-center mx-auto"></span>
);
}
// If the user is already authenticated, redirect to the home page
if (user) {
navigate("/");
}
// Handle form submission for user login
const handleFormSubmit = (e) => {
e.preventDefault();
const email = e.target.email.value;
const password = e.target.password.value;
loginUser(email, password)
.then((result) => {
console.log(result);
navigate("/");
})
.catch((error) => console.log(error.message));
e.target.reset();
};
// Render the login form
return (
<div>
<div className="min-h-screen bg-base-200">
<div className="hero-content flex-col">
<div className="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
<div className="card-body">
<form onSubmit={handleFormSubmit}>
<div className="form-control">
<label className="label">
<span className="label-text">Email</span>
</label>
<input
type="text"
name="email"
placeholder="Email"
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Password</span>
</label>
<input
type="password"
name="password"
placeholder="Password"
className="input input-bordered"
/>
</div>
<div className="form-control mt-6">
<button className="btn btn-primary">Login</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
);
};
export default Login;
Explanation:
The Login component utilizes the
useContext
hook to access theloginUser
,loading
, anduser
state from theAuthContext
.It checks if authentication is still loading, and if so, displays a loading indicator.
If the user is already authenticated (
user
is notnull
), it redirects them to the home page.The
handleFormSubmit
function is triggered when the user submits the login form. It retrieves the email and password from the form, calls theloginUser
function from the context, and handles success and error scenarios.The component renders a login form with email and password input fields.
SignUp Component
The SignUp component provides a form for users to create a new account.
import { useContext, useState } from "react";
import { AuthContext } from "./AuthProvider";
import { updateProfile } from "firebase/auth";
import { useNavigate } from "react-router-dom";
const SignUp = () => {
const { createUser, user, loading } = useContext(AuthContext);
const [selectedImage, setSelectedImage] = useState(null);
const navigate = useNavigate();
// If authentication is still loading, display a loading indicator
if (loading) {
return (
<span className="loading loading-dots loading-lg flex item-center mx-auto"></span>
);
}
// If the user is already authenticated, redirect to the home page
if (user) {
navigate("/");
}
// Handle form submission for user registration
const handleFormSubmit = (e) => {
e.preventDefault();
const name = e.target.name.value;
const email = e.target.email.value;
const password = e.target.password.value;
createUser(email, password)
.then((result) => {
// Update user profile with display name
updateProfile(result.user, {
displayName: name,
});
navigate("/");
console.log(result);
})
.catch((error) => {
console.log(error);
});
e.target.reset();
};
// Handle image upload (not shown in the code, but you can add it)
// Render the sign-up form
return (
<div>
<div className="min-h-screen bg-base-200">
<div className="hero-content flex-col">
<div className="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
<div className="card-body">
<form onSubmit={handleFormSubmit}>
<div className="form-control">
<label className="label">
<span className="label-text">Name</span>
</label>
<input
type="text"
name="name"
placeholder="Name"
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Email</span>
</label>
<input
type="email"
name="email"
placeholder="Email"
className="input input-bordered"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Password</span>
</label>
<input
type="password"
name="password"
placeholder="Password"
className="input input-bordered"
/>
</div>
<div className="form-control mt-6">
<button className="btn btn-primary">Sign Up</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
);
};
export default SignUp;
Explanation:
The SignUp component uses the
useContext
hook to access thecreateUser
,user
, andloading
state from theAuthContext
.It checks if authentication is still loading, and if so, displays a loading indicator.
If the user is already authenticated (
user
is notnull
), it redirects them to the home page.The
handleFormSubmit
function is triggered when the user submits the sign-up form. It retrieves the name, email, and password from the form, calls thecreateUser
function from the context to create a new account, and updates the user's profile with the provided name.The component renders a sign-up form with name, email, and password input fields. Note that the image upload part is not shown but can be added.
Profile Component
The Profile component displays the user's profile information.
import { useContext } from "react";
import { AuthContext } from "./AuthProvider";
const Profile = () => {
const { user } = useContext(AuthContext);
// Render user's profile information
return (
<div>
<div className="hero min-h-screen bg-base-200">
<div className="hero-content flex-col lg:flex-row">
<img
src="/images/stock/photo-1635805737707-575885ab0820.jpg"
className="max-w-sm rounded-lg shadow-2xl"
/>
<div>
<h1 className="text-5xl font-bold">{user?.displayName}</h1>
<p className="py-6">{user?.email}</p>
<button className="btn btn-primary">Get Started</button>
</div>
</div>
</div>
</div>
);
};
export default Profile;
Explanation:
The Profile component utilizes the
useContext
hook to access theuser
information from theAuthContext
.It renders the user's profile information, including their display name, email, and a "Get Started" button.
The user's profile picture can be displayed using the provided image source URL.
These components work together to provide a complete user authentication system in a React application using Firebase Authentication. The Header component handles navigation and user authentication status, while the Login, SignUp, and Profile components manage the respective functionalities for user authentication and profile management.
PrivateRoute Component
// PrivateRoute.js
import { useContext } from "react";
import { AuthContext } from "./AuthProvider";
import PropTypes from "prop-types";
import { Navigate } from "react-router-dom";
const PrivateRoute = ({ children }) => {
const { loading, user } = useContext(AuthContext);
if (loading) {
return <span className="loading loading-dots loading-lg"></span>;
}
if (user) {
return children;
}
return <Navigate to="/login" />;
};
PrivateRoute.propTypes = {
children: PropTypes.node,
};
export default PrivateRoute;
Setting Up Routing
Lastly, let's set up routing using the react-router-dom
library and wrap our app with the AuthProvider
.
// Routes.js
import { createBrowserRouter } from "react-router-dom";
import App from "./App";
import Login from "./components/Login";
import SignUp from "./components/SignUp";
import Profile from "./components/Profile";
import PrivateRoute from "./AuthProvider/PrivateRoute";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
path: "/login",
element: <Login />,
},
{
path: "/sign-up",
element: <SignUp />,
},
{
path: "/profile",
element: (
<PrivateRoute>
<Profile />
</PrivateRoute>
),
},
],
},
]);
export default router;
Conclusion
You've now created a Firebase Authentication system in a React app. You have authentication components for login, sign-up, and a user profile, as well as a PrivateRoute to protect routes that require authentication. Firebase provides a reliable and secure way to manage user authentication, making it a great choice for building user-centric applications.