Skip to main content

Welcome to a Grasp lesson

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

Lesson 81

Authentication-Based UI and Route Protection

Recommended resources

Hello! In the last lesson, you successfully transformed your task manager into a true full-stack application by connecting the React frontend to your Node.js API. Now, when you create, update, or delete a task, that change is persisted in your PostgreSQL database.

However, our application currently has a major limitation: it's a "one-for-all" system. Every visitor sees and can modify the exact same list of tasks. There's no concept of individual users, privacy, or security.

Today, we'll fix that. We are going to implement two fundamental features of any real-world application: protected routes and dynamic UI elements based on a user's authentication status. This will ensure that only logged-in users can access the main application, and the UI will intelligently adapt to show relevant options whether a user is logged in or out.

This lesson builds directly on the authentication context you've already set up. By the end, your application will feel much more professional and secure.

Part 1: The Concept of a Protected Route

In web applications, some pages are public (like a homepage or a login page), while others should only be accessible to authenticated users (like a user dashboard or settings page). A protected route is a route that enforces this rule.

The user experience should be seamless:

  1. An unauthenticated user tries to visit /dashboard.
  2. The application intercepts this, sees they aren't logged in, and redirects them to /login.
  3. Crucially, the app remembers that the user originally wanted to go to /dashboard.
  4. After the user successfully logs in, they are automatically redirected back to their intended destination: /dashboard.

Think of this as a guard function or a piece of middleware that sits in front of your route's component. It checks for credentials (in our case, a valid auth token from our context) before allowing access. If the check fails, it redirects the user elsewhere.

We will build a special React component to handle this logic.


Part 2: Implementing the ProtectedRoute Component

To implement protected routes, we'll create a reusable wrapper component. This component will use the authentication state from our AuthContext to decide whether to render its child component (the protected page) or redirect to the login page.

Let's watch a video that demonstrates this concept clearly using React Router v6. It refers to the component as RequireAuth, but we'll call ours ProtectedRoute.

React Protected Routes | Role-Based Authorization | React Router v6

This video from Dave Gray is an excellent guide to creating protected routes. It covers both the basic protection logic and the crucial part of redirecting the user back to their original destination after login.

Watch the following two segments: 10:04 to 13:56: This part covers the creation of the RequireAuth component. Pay close attention to how it uses useAuth to check the user's status, <Outlet> to render child routes, and <Navigate> to perform the redirect. Note how useLocation is used to pass the original path in the state. 15:53 to 18:49: This segment shows the other half of the logic: modifying the Login component to read the location state and navigate the user back to where they came from after a successful login. This is key for a good user experience.

Now, let's apply this to our project.

Step 2.1: Create the ProtectedRoute Component

Create a new file: src/components/ProtectedRoute.jsx.

This component will act as our guard. It will get the user's authentication status from your useAuth hook.

// src/components/ProtectedRoute.jsx
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext'; // Make sure the path is correct

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

  if (!user) {
    // If the user is not authenticated, redirect to the login page.
    // We pass the current location in the state so we can redirect back after login.
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  // If the user is authenticated, render the children components.
  return children;
};

export default ProtectedRoute;
React Router 6 Protected Routes Code Example
This image shows a similar implementation using `<Outlet />`. Our version using `children` is functionally equivalent and slightly more flexible for wrapping single components.

Breaking down the code:

  • useAuth(): This custom hook (which you should already have from a previous lesson) gives us access to the user object from our AuthContext.
  • useLocation(): This hook from react-router-dom gives us information about the current URL.
  • if (!user): This is our check. If user is null or undefined, the user is not authenticated.
  • <Navigate to="/login" ... />: This component from react-router-dom declaratively redirects the user.
    • state={{ from: location }}: This is the magic part. We're attaching the current location object to the navigation state. The login page will be able to access this.
    • replace: This prop tells the router to replace the current entry in the history stack. This way, if the user clicks the browser's back button from the login page, they won't get stuck in a redirect loop.

Step 2.2: Apply the ProtectedRoute to Your Routes

Now, open your main routing file (likely src/App.jsx) and wrap your protected routes with this new component.

// src/App.jsx (or wherever your routes are defined)
import { Routes, Route } from 'react-router-dom';
import DashboardPage from './pages/DashboardPage';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import ProtectedRoute from './components/ProtectedRoute';

// ...

function App() {
  return (
    <AuthProvider> {/* Assuming AuthProvider wraps your app */}
      <Routes>
        {/* Public Routes */}
        <Route path="/login" element={<LoginPage />} />
        <Route path="/register" element={<RegisterPage />} />
        
        {/* Protected Route */}
        <Route 
          path="/dashboard" 
          element={
            <ProtectedRoute>
              <DashboardPage />
            </ProtectedRoute>
          } 
        />

        {/* Add a root path redirect or a homepage */}
        <Route path="/" element={<LoginPage />} /> 
      </Routes>
    </AuthProvider>
  );
}

Step 2.3: Update the LoginPage

As shown in the video, the final step is to make your LoginPage "aware" of the from state.

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

const LoginPage = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const { login } = useAuth();
  
  // Get the 'from' location, or default to the dashboard
  const from = location.state?.from?.pathname || "/dashboard";

  const handleLoginSubmit = async (credentials) => {
    try {
      await login(credentials);
      // On successful login, navigate to the 'from' location
      navigate(from, { replace: true });
    } catch (error) {
      console.error("Login failed:", error);
      // Handle login error (e.g., show a message)
    }
  };

  // ... rest of your login form JSX and logic
  // Make sure your form's submit handler calls handleLoginSubmit
};

With these changes, your application now properly protects the dashboard and provides a smooth login-redirect flow for your users.


Part 3: Creating a Dynamic UI with Conditional Rendering

Protecting routes is only half the battle. Your UI should also adapt. A logged-in user shouldn't see "Login" and "Register" links; they should see a "Logout" button and perhaps their username.

This is a perfect use case for conditional rendering in React. There are several ways to do this, but the most common ones inside JSX are the ternary operator and the logical AND (&&) operator.

React 19 Tutorial - 10 - Conditional Rendering

This video from Codevolution provides a concise and clear explanation of the main conditional rendering techniques in React.

Watch the sections on the ternary operator (04:35 - 06:25) and the logical AND operator (06:25 - 08:28). These are the two patterns you'll use most frequently to build dynamic UIs.

Let's apply these patterns to a Header or Navbar component in our task manager.

Assume you have a Header.jsx component. You can use the useAuth hook to get the user's status and render different elements accordingly.

// src/components/Header.jsx
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

const Header = () => {
  const { user, logout } = useAuth();
  const navigate = useNavigate();

  const handleLogout = () => {
    logout();
    navigate('/login'); // Redirect to login page after logout
  };

  return (
    <header>
      <h1>Task Manager</h1>
      <nav>
        {user ? (
          // If user exists (is logged in)
          <>
            <span>Welcome, {user.email}!</span>
            <button onClick={handleLogout}>Logout</button>
          </>
        ) : (
          // If user does not exist (is not logged in)
          <>
            <Link to="/login">Login</Link>
            <Link to="/register">Register</Link>
          </>
        )}
      </nav>
    </header>
  );
};

export default Header;

Breaking down the conditional logic:

  • {user ? (...) : (...) }: This is the ternary operator. It checks if user is a truthy value. If it is, it renders the first block (the welcome message and logout button). If not, it renders the second block (the login and register links).
  • You could also use the logical AND && operator for simpler cases, like only showing the welcome message if the user is logged in: {user && <span>Welcome, {user.email}!</span>}. This reads as "if user is true, then render the span".
Test your understanding!

Imagine you want to add an "Admin" link to the header that is only visible to users who are logged in AND have a role of 'admin'. Assuming the user object from your useAuth hook looks like { email: '...', role: 'admin' }, how would you modify the navigation block to include this link?

Show answer

You would add a logical AND (&&) condition inside the authenticated user's view.

// Inside the Header component's return statement
<nav>
  {user ? (
    <>
      <span>Welcome, {user.email}!</span>
      {/* Add this line for the Admin link */}
      {user.role === 'admin' && <Link to="/admin">Admin Panel</Link>}
      <button onClick={handleLogout}>Logout</button>
    </>
  ) : (
    <>
      <Link to="/login">Login</Link>
      <Link to="/register">Register</Link>
    </>
  )}
</nav>

This is a powerful pattern for building role-based user interfaces.


Conclusion

Excellent work today! You've implemented a critical layer of security and user experience design in your application. It now behaves much more like a professional, multi-user web app.

Key Takeaways:

  • You built a ProtectedRoute component to guard parts of your application, ensuring only authenticated users can access them.
  • You learned how to create a seamless redirect flow, sending users to their intended destination after they log in.
  • You mastered conditional rendering techniques (ternary and &&) to create a dynamic UI that adapts based on the user's authentication status.

Preview of the Next Lesson:
Our application is now secure at the route level, but the data itself is still communal. A logged-in user can still see and edit tasks created by everyone else. In the next lesson, we will integrate authentication with our CRUD operations. You will modify your backend and frontend to ensure that tasks are associated with specific users, and users can only view and manage their own tasks. This is the final step in creating a truly personalized task manager.