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:
- An unauthenticated user tries to visit
/dashboard. - The application intercepts this, sees they aren't logged in, and redirects them to
/login. - Crucially, the app remembers that the user originally wanted to go to
/dashboard. - 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;

Breaking down the code:
useAuth(): This custom hook (which you should already have from a previous lesson) gives us access to theuserobject from ourAuthContext.useLocation(): This hook fromreact-router-domgives us information about the current URL.if (!user): This is our check. Ifuserisnullorundefined, the user is not authenticated.<Navigate to="/login" ... />: This component fromreact-router-domdeclaratively redirects the user.state={{ from: location }}: This is the magic part. We're attaching the currentlocationobject 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 ifuseris 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 "ifuseris 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
ProtectedRoutecomponent 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 (
ternaryand&&) 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.