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 app
  • react-router-dom – provides routing capabilities
  • @mui/material, @emotion/react, @emotion/styled – the Material UI library for pre-styled components
  • react-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.

Firebase Add App

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!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *