Frontend Development 13 min read

Unified Route, Menu, and Breadcrumb Configuration for a React + Ant Design Admin Project

This article demonstrates how to centralize route, menu, permission, and breadcrumb definitions in a React and Ant Design based admin application by using nested configuration objects, flattening utilities, and dynamic lookup functions to reduce duplication and simplify maintenance.

ByteFE
ByteFE
ByteFE
Unified Route, Menu, and Breadcrumb Configuration for a React + Ant Design Admin Project

In a typical React admin project that uses react-router-config and Ant Design, route, menu, permission, and breadcrumb information are often declared in multiple places, leading to repetitive code. The article proposes defining all related data in a single configuration object and retrieving it dynamically where needed.

Individual Configuration

Route and Permission

Routes are declared with react-router-config as shown below:

// router.ts
import { RouteConfig } from 'react-router-config';
import DefaultLayout from './layouts/default';
import GoodsList from './pages/goods-list';
import GoodsItem from './pages/goods-item';

export const routes: RouteConfig[] = [
  {
    component: DefaultLayout,
    routes: [
      {
        path: '/goods',
        exact: true,
        title: '商品列表',
        component: GoodsList,
      },
      {
        path: '/goods/:id',
        exact: true,
        title: '商品详情',
        component: GoodsItem,
      }
    ],
  },
];

// app.tsx
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import { routes } from './router';

export default function App() {
  return
{renderRoutes(routes)}
;
}

Menu

The left navigation uses Ant Design's <Menu /> component:

// ./layouts/default
import React from 'react';
import { renderRoutes } from 'react-router-config';
import { Layout, Menu } from 'antd';

export default function({ route }) {
  return (
Header
商品列表
{renderRoutes(route.routes)}
);
}

Permission

Each page requests a permission list from the backend; the permission key is stored alongside the route configuration and checked on navigation:

// router.ts (extended)
export const routes: RouteConfig[] = [
  {
    component: DefaultLayout,
    routes: [
      {
        path: '/goods',
        exact: true,
        title: '商品列表',
        component: GoodsList,
        permission: 'goods',
      },
      {
        path: '/goods/:id',
        exact: true,
        title: '商品详情',
        component: GoodsItem,
        permission: 'goods-item',
      }
    ],
  },
];

// ./layouts/default (permission check)
import React, { useEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { matchRoutes } from 'react-router-config';

export default function({ route }) {
  const history = useHistory();
  const location = useLocation();
  const page = useMemo(() =>
    matchRoutes(route.routes, location.pathname)?.[0]?.route,
    [location.pathname, route.routes]
  );

  useEffect(() => {
    getPermissionList().then(permissions => {
      if (page.permission && !permissions.includes(page.permission)) {
        history.push('/no-permission');
      }
    });
  }, []);
}

Breadcrumb

Breadcrumbs are rendered with Ant Design's <Breadcrumb /> component:

// ./pages/goods-item.tsx
import React from 'react';
import { Link } from 'react-router-dom';
import { Breadcrumb } from 'antd';

export default function() {
  return (
商品列表
商品详情
);
}

Merged Configuration

All related data (route, menu flag, permission, children) are placed in a nested configuration structure, which can be flattened when needed:

// router-config.ts
import type { RouterConfig } from 'react-router-config';
import GoodsList from './pages/goods-list';
import GoodsItem from './pages/goods-item';

export interface PathConfig extends RouterConfig {
  menu?: boolean;
  permission?: string;
  children?: PathConfig[];
}

export const routers = [
  {
    path: '/goods',
    exact: true,
    title: '商品列表',
    component: GoodsList,
    children: [
      {
        path: '/goods/:id',
        exact: true,
        title: '商品详情',
        component: GoodsItem,
      }
    ]
  }
];

A helper flatRouters() converts the nested array into a flat list for react-router-config :

// router.ts
import { RouteConfig } from 'react-router-config';
import DefaultLayout from './layouts/default';
import { routers, PathConfig } from './router-config';

function flatRouters(routers: PathConfig[]): PathConfig[] {
  const results = [];
  for (let i = 0; i < routers.length; i++) {
    const { children, ...router } = routers[i];
    results.push(router);
    if (Array.isArray(children)) {
      results.push(...flatRouters(children));
    }
  }
  return results;
}

export const routes: RouteConfig[] = [
  {
    component: DefaultLayout,
    routes: flatRouters(routers),
  },
];

Menu rendering iterates over the same configuration, showing only items with menu: true :

// ./layouts/default (menu component)
import React from 'react';
import { Layout, Menu } from 'antd';
import { routers } from './router-config';

const NavMenu = () => (
{routers.filter(({ menu }) => menu).map(({ title, path, children }) => (
      Array.isArray(children) && children.some(c => c.menu) ? (
{children.filter(c => c.menu).map(({ title, path }) => (
))}
) : (
)
    ))}
);

const NavMenuItem = ({ path, title }) => (
{/^https?:/.test(path) ? (
{title}
) : (
{title}
    )}
);

export default function({ route }) {
  return (
Header
{renderRoutes(route.routes)}
);
}

Breadcrumb Search

To build breadcrumbs, a findCrumb() function walks the nested configuration based on the current pathname, handling dynamic parameters by converting them to regex patterns. The resulting array of matched routes is then rendered as breadcrumb items.

// breadcrumb.tsx
import React, { useMemo } from 'react';
import { Breadcrumb as OBreadcrumb, BreadcrumbProps } from 'antd';
import { useHistory, useLocation, useParams } from 'react-router';
import Routers, { PathConfig } from '../router-config';

function findCrumb(routers: PathConfig[], pathname: string): PathConfig[] {
  const ret: PathConfig[] = [];
  const router = routers
    .filter(({ path }) => path !== '/')
    .find(({ path }) =>
      new RegExp(`^${path.replace(/:[a-zA-Z]+/g, '.+?').replace(/\//g, '\/')}`, 'i').test(pathname)
    );
  if (!router) return ret;
  ret.push(router);
  if (Array.isArray(router.children)) {
    ret.push(...findCrumb(router.children, pathname));
  }
  return ret;
}

function stringify(path: string, params: Record
) {
  return path.replace(/:([a-zA-Z]+)/g, (placeholder, key) => params[key] || placeholder);
}

const Breadcrumb = React.memo
(props => {
  const history = useHistory();
  const params = useParams();
  const location = useLocation();

  const routers = useMemo(() => findCrumb(Routers, location.pathname).slice(1), [location.pathname]);

  if (!routers.length || routers.length < 2) return null;

  const data = props.data ? props.data : routers.map(({ title: name, path }, idx) => ({
    name,
    onClick: idx !== routers.length - 1 ? () => history.push(stringify(path, params)) : undefined,
  }));

  return (
{data.map(({ name, onClick }) => (
{name}
))}
);
});

export default Breadcrumb;

Afterword

By adding a few extra fields (menu flag, permission, children) to the route definition, all configuration can be centralized, making it easier to store in a backend system for dynamic menu management. The approach may not suit every scenario—for example, micro‑frontend architectures often require separate configurations for host and remote applications.

References

react‑router‑config: https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config

Ant Design Menu component: https://ant.design/components/menu-cn/

reactConfigurationRoutingReact RouterAnt DesignBreadcrumbmenu
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.