Building a React Higher-Order Component for Route Authentication and Nested Routing
Route Guard Implementation Overview
Modern React applications often require authentication-aware routing. A higher-order component approach provides a clean separation of concerns, encapsulating all authentication logic in a reusable wrapper that can protect routes across your application.
The implementation consists of three core pieces: a route guard HOC that evaluates authentication state, a route configuration file defining all top-level paths, and parent components that define nested child routes.
RouteGuard Component
The guard component intercepts navigation attempts and determines access based on login status. It examines the current pathname against registered routes, distinguishing between top-level and nested paths.
import React, { Component } from "react";
import { Route, Redirect, Switch } from "react-router-dom";
import { getSessionToken } from "../utils/session.js";
class RouteAuthenticator extends Component {
render() {
const { routesConfig, currentLocation } = this.props;
const { pathname } = currentLocation;
// Extract top-level path segment for matching
const topLevelPath = "/" + pathname.split("/")[1];
// Verify authentication state
const hasValidSession = getSessionToken("admin_panel_session");
// Find matching route configuration
const matchedRoute = routesConfig.find((route) => {
const normalizedPath = route.path.replace(/\s*/g, "");
return topLevelPath === normalizedPath;
});
if (hasValidSession) {
// Authenticated user behavior
if (pathname === "/signin" || pathname === "/") {
return <Redirect to="/dashboard" />;
}
if (matchedRoute) {
return (
<Route
path={pathname}
exact={true}
component={matchedRoute.component}
/>
);
}
return <Redirect to="/not-found" />;
}
// Unauthenticated user behavior
if (pathname === "/") {
return <Redirect to="/signin" />;
}
if (matchedRoute && !matchedRoute.requiresAuth) {
const { component } = matchedRoute;
return <Route exact path={pathname} component={component} />;
}
if (matchedRoute && matchedRoute.requiresAuth) {
return <Redirect to="/signin" />;
}
return <Redirect to="/not-found" />;
}
}
export default RouteAuthenticator;
The key mechanism here involves extracting only the first path segment for route matching. This approach allows child routes to inherit their parent component without triggering unauthorized redirects. When /dashboard/user-management is accessed, the guard matches /dashboard rather than the full nested path, enabling proper parent component rendering with nested content.
Route Configuration Structure
Define all top-level routes in a centralized configuration file. Each route entry specifies its path, associated component, and whether authentication is required for access.
import Dashboard from "../views/dashboard/dashboard.jsx";
import SignIn from "../views/auth/signin.jsx";
import PageNotFound from "../views/errors/not-found.jsx";
const applicationRoutes = [
{
path: "/dashboard",
label: "dashboard",
component: Dashboard,
requiresAuth: true
},
{
path: "/signin",
label: "signin",
component: SignIn,
requiresAuth: false
},
{
path: "/not-found",
label: "notFound",
component: PageNotFound,
requiresAuth: false
},
];
export default applicationRoutes;
The requiresAuth flag determines whether the route remains accessible without authentication. Public routes like sign-in and error pages set this flag to false, while protected resources require a valid sestion.
Application Entry Point
Integrate the guard component at the root level, passing the route configuration as a prop. The component wraps all application routes, providing centralized authentication enforcement.
import React from "react";
import { BrowserRouter, Switch } from "react-router-dom";
import routeDefinitions from "./config/routes.js";
import RouteAuthenticator from "./guards/route-guard.js";
function Application() {
return (
<div className="app-container">
<BrowserRouter>
<Switch>
<RouteAuthenticator routesConfig={routeDefinitions} />
</Switch>
</BrowserRouter>
</div>
);
}
export default Application;
Implementing Nested Routes in Parent Components
Parent components hosting child routes must render nested Route definitions within a Switch component. This pattern enables conditional rendering of child components based on the current URL while maintaining the parent layout.
import React from "react";
import { Drawer, NavBar, Notification } from "antd";
import "./dashboard-layout.less";
import UserAdmin from "../../modules/user-administration/user-list.jsx";
import SystemTheme from "../../modules/themes/theme-settings.jsx";
import RoleAdmin from "../../modules/role-administration/role-list.jsx";
import ResourceAdmin from "../../modules/resource-administration/resource-list.jsx";
import { Route, Redirect, Switch } from "react-router-dom";
import { Button, Layout } from "antd";
import { clearSession } from "../../utils/session";
const { Content, Sider } = Layout;
export default class DashboardLayout extends React.Component {
sidebarState = {
collapsed: false,
pageTitle: "System Dashboard"
};
toggleSidebar = () => {
this.setState(({ collapsed }) => ({
collapsed: !collapsed
}));
};
navigateToRoute = (routePath, titleValue) => {
this.props.history.push(routePath);
setTimeout(() => {
this.setState({ pageTitle: titleValue });
}, 100);
};
componentDidMount() {
const currentPath = this.props.location.pathname;
this.updateTitleFromPath(currentPath);
}
updateTitleFromPath = (path) => {
const titleMap = {
"/dashboard": "System Dashboard",
"/dashboard/users": "User Management",
"/dashboard/roles": "Role Administration",
"/dashboard/resources": "Resource Management"
};
this.setState({
pageTitle: titleMap[path] || "System Dashboard"
});
};
handleSignOut = () => {
clearSession("admin_panel_session");
Notification.success({ message: "Successfully signed out" });
this.props.history.push("/signin");
};
renderNavigation() {
const menuItems = [
{
key: "/dashboard",
icon: "dashboard",
label: "Dashboard",
title: "System Dashboard"
},
{
key: "/dashboard/users",
icon: "team",
label: "User Management",
title: "User Management"
},
{
key: "/dashboard/roles",
icon: "safety",
label: "Role Administration",
title: "Role Administration"
},
{
key: "/dashboard/resources",
icon: "database",
label: "Resource Management",
title: "Resource Management"
},
];
return (
<div className="navigation-panel">
<div className="brand-header">
<span className="logo-text">Admin Portal</span>
</div>
<span className="section-label">Administration</span>
<ul className="nav-menu">
{menuItems.map(item => (
<li
key={item.key}
className={`nav-item ${this.props.location.pathname === item.key ? "active" : ""}`}
onClick={() => this.navigateToRoute(item.key, item.title)}
>
<i className={`icon-${item.icon}`} />
<span>{item.label}</span>
</li>
))}
</ul>
<div className="footer-actions">
<Button
type="primary"
danger
onClick={this.handleSignOut}
block
>
Sign Out
</Button>
</div>
</div>
);
}
render() {
return (
<Layout className="dashboard-wrapper" style={{ height: "100vh" }}>
<Sider
className="sidebar-panel"
width={260}
collapsible
collapsed={this.state.collapsed}
onCollapse={this.toggleSidebar}
>
{this.renderNavigation()}
</Sider>
<Layout>
<NavBar
className="top-navigation"
mode="light"
icon={<i className="icon-menu" />}
onLeftClick={this.toggleSidebar}
>
<span className="page-heading">{this.state.pageTitle}</span>
</NavBar>
<Content className="content-region">
<Switch>
<Route path="/dashboard" exact component={SystemTheme} />
<Route path="/dashboard/users" exact component={UserAdmin} />
<Route path="/dashboard/roles" exact component={RoleAdmin} />
<Route path="/dashboard/resources" exact component={ResourceAdmin} />
<Redirect from="/dashboard/*" to="/not-found" />
</Switch>
</Content>
</Layout>
</Layout>
);
}
}
The parent dashboard component defines its own nested routes using Switch. The catch-all redirect pattern (/dashboard/* to /not-found) ensures that unauthorized nested paths fall back to a 404 page rather than rendering unexpected content. This architecture creates a clear hierarchy where authentication happens at the top level while individual components manage their specific nested routes.