Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Building a React Higher-Order Component for Route Authentication and Nested Routing

Notes May 9 3

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.

Related Articles

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Spring Boot MyBatis with Two MySQL DataSources Using Druid

Required dependencies application.properties: define two data sources and poooling Java configuration for both data sources MyBatis mappers for each data source Controller endpoints to verify both co...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.