Routing is one of the first places where a small React application becomes a real application. A production application usually has public pages, authenticated pages, nested layouts, route parameters, query parameters, 404 handling, redirects, role-based authorization, and a login flow that remembers the page the user originally requested. This chapter builds those concepts from the basics into a complete example.
What routing actually solves
Without a routing library, an application has to manually decide which component should appear for a particular URL. React Router gives the application a structured way to map URLs to React components while keeping the SPA experience.
flowchart LR
A[Browser URL] --> B[Router]
B --> C[Match route]
C --> D[Render layout]
D --> E[Render page]
E --> F[Update browser history]
Install and project structure
A practical project might be organized like this:
src/
app/
App.jsx
auth/
AuthContext.jsx
ProtectedRoute.jsx
layouts/
AppLayout.jsx
AdminLayout.jsx
pages/
Home.jsx
Login.jsx
Dashboard.jsx
Orders.jsx
OrderDetails.jsx
AdminUsers.jsx
NotFound.jsx
main.jsxThe important idea is separation of concerns: route definitions describe navigation, authentication manages identity, and pages contain screen-level behavior.
Basic route configuration
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/orders" element={<Orders />} />
<Route path="/orders/:orderId" element={<OrderDetails />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}React Router checks the current URL and renders the element belonging to the matching route.
Link vs normal anchor
For navigation inside an SPA, use React Router's Link or NavLink. A normal anchor can cause the browser to perform a full document navigation.
import { Link, NavLink } from "react-router-dom";
<Link to="/orders">Orders</Link>
<NavLink to="/dashboard">
Dashboard
</NavLink>NavLink is especially useful for navigation menus because it can expose whether the route is active.
Programmatic navigation
After a successful login, save operation, or workflow completion, the application may need to navigate from code.
import { useNavigate } from "react-router-dom";
function CreateOrder() {
const navigate = useNavigate();
async function handleSubmit() {
const order = await saveOrder();
navigate(`/orders/${order.id}`);
}
return <button onClick={handleSubmit}>Create Order</button>;
}
Route parameters
Route parameters are ideal when the URL identifies a particular resource.
<Route
path="/orders/:orderId"
element={<OrderDetails />}
/>
function OrderDetails() {
const { orderId } = useParams();
return <h2>Order ID: {orderId}</h2>;
}For /orders/5001, the component receives orderId = 5001.
Query parameters
Query parameters are commonly used for filters, search, sorting, and pagination.
// URL:
// /orders?status=approved&page=2
const [params] = useSearchParams();
const status = params.get("status");
const page = params.get("page");This keeps filter state represented in the URL, which makes the page shareable and bookmarkable.
Nested routes and shared layouts
Large applications usually have a layout around a group of screens. Nested routes allow the layout to remain mounted while only the child page changes.
<Route path="/app" element={<AppLayout />}>
<Route index element={<Dashboard />} />
<Route path="orders" element={<Orders />} />
<Route path="orders/:orderId" element={<OrderDetails />} />
<Route path="settings" element={<Settings />} />
</Route>import { Outlet } from "react-router-dom";
function AppLayout() {
return (
<>
<Header />
<Sidebar />
<main>
<Outlet />
</main>
</>
);
}
Authentication state
Authentication should be represented separately from routing. A simple auth context can expose the current user and login/logout operations to the rest of the application.
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
async function login(credentials) {
const result = await loginApi(credentials);
setUser(result.user);
}
function logout() {
setUser(null);
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
return useContext(AuthContext);
}
Protected route
A protected route checks authentication before rendering the private page. If the user is not authenticated, it redirects to login.
function ProtectedRoute({ children }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return (
<Navigate
to="/login"
replace
state={{ from: location }}
/>
);
}
return children;
}
Complete protected route configuration
<Route path="/login" element={<Login />} />
<Route
path="/app"
element={
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
}
>
<Route index element={<Dashboard />} />
<Route path="orders" element={<Orders />} />
<Route path="orders/:orderId" element={<OrderDetails />} />
</Route>Now the entire /app route tree is protected instead of repeating authentication logic in every page.
Login and return to original page
A common production experience is: the user opens /app/orders, is redirected to login, signs in successfully, and is returned to /app/orders.
function Login() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
async function handleLogin(credentials) {
await login(credentials);
const from = location.state?.from?.pathname || "/app";
navigate(from, { replace: true });
}
// form omitted
}
Authentication vs authorization
Authentication answers who the user is. Authorization answers what that authenticated user can do. A protected route based only on authentication is not enough for an administrator page.
function AdminRoute({ children }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
if (!user.permissions.includes("users.manage")) {
return <Forbidden />;
}
return children;
}
Role-based routing
Some systems use roles such as Admin, Manager, and Employee. Others use permissions. Permissions are often more flexible because a user can have several independent capabilities.
const user = {
id: 101,
name: "Ravi",
role: "Manager",
permissions: [
"orders.read",
"orders.create",
"reports.read"
]
};
Important security rule
Frontend route protection is a user-interface mechanism, not a security boundary. A user can directly call an API without using your React UI. The backend must independently validate authentication and authorization for every protected operation.
flowchart TD
A[React protected route] --> B[User sees private page]
C[Direct API request] --> D[Backend authentication]
D --> E[Backend authorization]
E --> F[Allow or deny]
404 handling
A catch-all route provides a proper page for unknown URLs.
<Route
path="*"
element={<NotFound />}
/>
Complete example output
Imagine the following navigation:
- User opens
/app/orders/5001. - Router matches the protected app route and order-detail route.
- If the user is not logged in, the user is redirected to
/login. - After successful login, the application returns to
/app/orders/5001. - The order page reads
5001from the route parameter. - The backend still checks the user's authorization before returning order data.
Browser URL:
http://localhost:5173/app/orders/5001
Visible output:
Order Details
Order ID: 5001
Status: Approved
Customer: ABC Corporation
Common production mistakes
- Checking authentication only in the UI and not on the API.
- Duplicating protected-route logic in every component.
- Using local component state for authentication that disappears unexpectedly.
- Not preserving the requested URL after login.
- Using roles everywhere when fine-grained permissions are actually needed.
- Putting every route in one huge unreadable file.
Interview questions
- How does React Router work?
- What is the difference between Link and an anchor?
- How do you implement protected routes?
- Authentication vs authorization?
- How do nested routes and Outlet work?
- How do you preserve the original URL after login?
- Why can frontend route protection never replace backend authorization?
Key takeaways
- Use routing to map URLs to screens.
- Use nested routes for shared application layouts.
- Protect private route trees instead of duplicating checks.
- Separate authentication from authorization.
- Always enforce authorization on the backend.