Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 63

Building a Protected Route Component

Recommended resources

Hello! Let's dive into our next topic.

In the previous lesson, we saw how to use the useNavigate hook to programmatically redirect users after an event, like submitting a form. We also learned how to pass information using the location.state object. These skills are the building blocks for one of the most essential features in any web application: authentication and protected routes.

Today, you'll learn how to create a protected route component to restrict access for unauthenticated users. We'll build a component that acts as a guard, checking if a user is logged in before allowing them to see a specific page. If they aren't, we'll redirect them to a login page, and—crucially—we'll remember where they were trying to go so we can send them back there after they log in.

1. The Logic of a Protected Route

At its core, protecting a route is about making a decision. When a user requests a page, our application needs to answer a few questions:

  1. Is this route private?
  2. If so, is the user logged in?
  3. If they are logged in, show them the page.
  4. If they are not logged in, redirect them to the login page.

This flow is universal for almost any application with a login system.

This flowchart provides a great visual overview of the entire process, including the "redirect back" logic we'll implement.

Protected Route Logic Flowchart
This flowchart illustrates the decision-making process for handling protected routes. It shows how the app checks for authentication, rendering the requested page for logged-in users and redirecting others to a login page, while remembering their original destination.

To implement this, we need two key things:

  1. A way to manage and share the user's authentication status across our entire app.
  2. A special "wrapper" component that contains the logic from the flowchart.

2. Managing Authentication State with Context

A user's login status is a piece of global state. Multiple components might need to know if the user is logged in: the navbar (to show "Login" or "Logout"), the profile page, and of course, our protected route component.

Passing this state down through props would be cumbersome (a problem known as "prop drilling"). A much better solution is React's Context API, which allows you to share state across the component tree without passing props down manually at every level.

We will create an AuthContext to provide the user's status and login/logout functions to any component that needs them.

The following video provides an excellent, step-by-step guide to setting up an authentication context.

React Router Tutorial - 15 - Authentication and Protected Routes

Let's watch a segment from Codevolution's "React Router Tutorial - 15 - Authentication and Protected Routes". This part focuses on creating a simple but effective system for managing user authentication state using React's useState and Context API.

Watch from 03:04 to 06:29. As you watch, focus on: The creation of the AuthContext using createContext. The AuthProvider component that holds the user state and the login/logout functions. The creation of the useAuth custom hook, which simplifies accessing the context's value.

To summarize the video, you would create a file like src/context/AuthContext.jsx:

// src/context/AuthContext.jsx
import { useState, createContext, useContext } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null); // `null` means logged out

  const login = (username) => {
    setUser(username);
  };

  const logout = () => {
    setUser(null);
  };

  // The value provided to the context consumers
  const value = { user, login, logout };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Custom hook to use the auth context
export function useAuth() {
  return useContext(AuthContext);
}

Then, you would wrap your entire application in App.js or main.jsx with the AuthProvider to make the context available everywhere:

// In main.jsx or App.js
import { AuthProvider } from './context/AuthContext';

//...
<AuthProvider>
  <App />
</AuthProvider>
//...

3. Creating the ProtectedRoute Component

Now for the main event. We'll create a component that acts as our guard. In React Router v6, the cleanest way to do this is by using the <Outlet> component, which you've already seen in our lesson on nested routes.

Our ProtectedRoute component will do the following:

  • Use our useAuth hook to check if a user is logged in.
  • If they are, it will render an <Outlet />, which represents any nested child routes.
  • If they are not, it will use the <Navigate /> component to redirect them to the login page.

This image shows the core idea perfectly. If a token (our user state) exists, render <Outlet />; otherwise, render <Navigate />.

React Protected Routes Implementation Example
This code snippet shows a `PrivateRoutes` component using an authentication token to either render child routes via `<Outlet />` or redirect to the login page with `<Navigate />`.

Let's create this component.

// src/components/ProtectedRoute.jsx
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

function ProtectedRoute() {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    // Redirect them to the /login page, but save the current location they were
    // trying to go to. This allows us to send them along to that page after they
    // log in, which is a nicer user experience than dropping them off on the home page.
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <Outlet />;
}

export default ProtectedRoute;

Let's break down two important parts of that <Navigate> component:

  • state={{ from: location }}: This is where we use the concept from our last lesson! We're passing the current location object (which contains the path the user was trying to visit, e.g., /profile) through the state prop. The login page will be able to access this.
  • replace: This tells React Router to replace the current entry in the history stack. This is crucial. When the user is redirected to the login page, we don't want them to be able to click the browser's "back" button and end up on the protected page they were just kicked out of.

4. Configuring the Routes

With our AuthProvider and ProtectedRoute components ready, we can now configure our routes in App.js. The structure is very intuitive: we create a parent <Route> that uses our ProtectedRoute as its element, and then nest the actual protected routes inside it.

// src/App.js
import { Routes, Route } from 'react-router-dom';
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage';
import ProfilePage from './pages/ProfilePage'; // A dummy protected page
import ProtectedRoute from './components/ProtectedRoute';

function App() {
  return (
    <Routes>
      {/* Public Routes */}
      <Route path="/" element={<HomePage />} />
      <Route path="/login" element={<LoginPage />} />

      {/* Protected Routes */}
      <Route element={<ProtectedRoute />}>
        <Route path="/profile" element={<ProfilePage />} />
        {/* You can add more protected routes here */}
        {/* <Route path="/dashboard" element={<DashboardPage />} /> */}
      </Route>
    </Routes>
  );
}

export default App;

This setup is clean and scalable. Any route nested inside the ProtectedRoute element will automatically be guarded.


5. Completing the Loop: The Login Page

The final piece of the puzzle is the LoginPage. It needs to do two things:

  1. Call the login function from our AuthContext on successful authentication.
  2. Navigate the user to their intended destination (location.state.from) or to the homepage as a fallback.

The next video demonstrates exactly how to implement the login page logic and the "redirect back" feature.

React Router Tutorial - 15 - Authentication and Protected Routes

Let's return to the Codevolution video. This final segment demonstrates how to tie everything together by building the login page and, most importantly, redirecting the user back to the page they originally wanted to visit.

Watch from 06:29 to 08:48 and then from 15:31 to 17:38. These sections cover: Creating a simple login form. Using the useNavigate and useLocation hooks in the login component. Checking for location.state.from to determine the redirect path. Using the replace option on navigation to improve browser history behavior.

Here's what the code for a LoginPage.jsx would look like, based on the principles in the video:

// src/pages/LoginPage.jsx
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

function LoginPage() {
  const [username, setUsername] = useState('');
  const navigate = useNavigate();
  const location = useLocation();
  const { login } = useAuth();

  // Get the path to redirect to from location state, or default to homepage
  const from = location.state?.from?.pathname || '/';

  const handleSubmit = (event) => {
    event.preventDefault();
    // In a real app, you'd have validation and an API call here
    if (username) {
      login(username);
      // Send them back to the page they tried to visit before being redirected
      navigate(from, { replace: true });
    }
  };

  return (
    <div>
      <h1>Login</h1>
      <form onSubmit={handleSubmit}>
        <label>
          Username:
          <input 
            type="text" 
            value={username} 
            onChange={(e) => setUsername(e.target.value)} 
            required 
          />
        </label>
        <button type="submit">Login</button>
      </form>
    </div>
  );
}

export default LoginPage;

Now, if a user tries to go to /profile, they'll be sent to /login. After they enter a username and click "Login", they'll be seamlessly redirected to /profile.


Conclusion

You have just implemented one of the most fundamental patterns in modern web development. By creating a reusable protected route component, you can easily secure parts of your application and provide a smooth, professional user experience.

Key Takeaways:

  • Protected Routes are wrapper components that guard access to their children based on a condition, like authentication.
  • React Context is the ideal tool for managing global state like a user's authentication status. A custom useAuth hook makes it easy to consume this context.
  • The modern pattern in React Router v6 uses a parent <Route> with the protected component as its element, which then renders an <Outlet /> for authenticated users.
  • The <Navigate /> component is a declarative way to redirect unauthenticated users.
  • To create a good user experience, use useLocation and the state prop on <Navigate> to remember the user's intended destination and redirect them there after they log in.

Next Up:

Our login function is currently just a simulation. In a real-world application, logging in involves making a network request to a backend server to verify credentials. In the next module, we will explore how to work with APIs in React, fetching data and managing loading/error states. This will allow you to replace our simulated authentication with a real one.