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.
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/
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.