Part 3 --- Protected Routes & Authorization (Frontend) Frontend route… — Full Stack Camp — TG.ME

Part 3 --- Protected Routes & Authorization (Frontend)

Frontend route protection is the user-facing gatekeeper of your application, ensuring unauthorized visitors cannot manually type URLs to access restricted dashboards or admin panels. The pattern involves creating a wrapper component, conventionally named ProtectedRoute, that checks the global authentication state—usually sourced from a Context or Zustand store that synchronizes with localStorage. While the authentication status is being verified (for instance, if the token exists but we haven't fetched the user's profile yet), the wrapper renders a loading spinner to avoid the visual flicker of redirecting from a login page. If the user is authenticated, the wrapper renders its child components (typically using Outlet in React Router v6 for nested routes). If not, it imperatively navigates the user back to the login screen using useNavigate, creating a seamless and secure browsing experience.

Creating an Auth Context (Global User State)

We need a global state to hold the current user and loading status. We'll use the Context API (or Zustand) so that the Navbar, ProtectedRoute, and any component can access the authentication status instantly.

// context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from "react";
import { getProfile } from "../services/authService";

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
// Check if a token exists on app mount
const token = localStorage.getItem("accessToken");
if (token) {
// Verify the token by fetching the user profile
getProfile()
.then((userData) => setUser(userData))
.catch(() => {
// If token is invalid, clear it
localStorage.removeItem("accessToken");
localStorage.removeItem("user");
setUser(null);
})
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);

const login = (userData, token) => {
localStorage.setItem("accessToken", token);
localStorage.setItem("user", JSON.stringify(userData));
setUser(userData);
};

const logout = () => {
localStorage.removeItem("accessToken");
localStorage.removeItem("user");
setUser(null);
};

return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => useContext(AuthContext);

Implementing the ProtectedRoute Component

This component uses the useAuth hook to determine if the user is authenticated. If loading is true, we show a spinner. If user is null, we redirect to /login. Otherwise, we render the child routes.

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

const ProtectedRoute = () => {
const { user, loading } = useAuth();

if (loading) {
return <div className="spinner">Loading your session...</div>;
}

return user ? <Outlet /> : <Navigate to="/login" replace />;
};

export default ProtectedRoute;

Using in App.jsx

We structure our routes so that all private pages are nested inside the <ProtectedRoute> component.

// App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import ProtectedRoute from "./components/ProtectedRoute";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import Profile from "./pages/Profile";

function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Route>
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
August 7, 2026 84