You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Router组件渲染错误:CRM系统开发中的路由问题

React Router组件渲染错误解决方案

问题描述

开发自定义CRM系统时触发React Router报错:

BrowserRouter is not a component. All component children of must be a or <React.Fragment>

相关代码如下:

import {AppBar, Box, CssBaseline, Divider, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Toolbar, Typography, ListSubheader, Breadcrumbs, Link, styled} from '@mui/material'
import StackedLineChartIcon from '@mui/icons-material/StackedLineChart';
import PeopleOutlineIcon from '@mui/icons-material/PeopleOutline';
import WorkOutlineIcon from '@mui/icons-material/WorkOutline';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import HomeIcon from '@mui/icons-material/Home';

import { useState,useMemo, Fragment } from 'react';
import { BrowserRouter as Route, Routes, Router } from "react-router-dom";

import EmployeeDashboard from "../main-module/components/EmployeeDashboard";
import CollapsibleTable from '../main-module/components/ClientsList';
import EmployeeStats from '../main-module/components/EmployeeStats';
import ProfileMenu from '../main-module/components/ProfileMenu';
import EmployeeOptions from '../main-module/components/EmployeeOptions';

const drawerWidth = 240;

const StyledListItemBtn = styled(ListItemButton)(({ theme }) => ({
  borderRadius: "4px"
}));

const WorkPage = () => {
    const [component, setComponent] = useState("call");
    const [selectedIdx, setSelectedIdx] = useState(2);

    const handleItemClick = (idx) => {
        setSelectedIdx(idx);
    };

    const drawer = (
        <Box >
            <Toolbar sx={{ display: "flex", alignItems: "center", justifyContent: "space-around" }}>
                <Typography variant='h5'>office.kr</Typography>
            </Toolbar>
            <Divider />
                <List       
                subheader={
                    <ListSubheader>CRM</ListSubheader>
                }>
                    <ListItem>
                        <StyledListItemBtn 
                            selected={selectedIdx === 0} 
                            onClick={() => {setComponent("stats"); handleItemClick(0)}}
                        >
                            <ListItemIcon>
                                    <StackedLineChartIcon color='secondary'/>
                            </ListItemIcon>
                            <ListItemText primary="Моя статистика" />
                        </StyledListItemBtn>
                    </ListItem>
                    <ListItem>
                        <StyledListItemBtn 
                            selected={selectedIdx === 1} 
                            onClick={() => {setComponent("clients"); handleItemClick(1)}}
                        >
                            <ListItemIcon>
                                    <PeopleOutlineIcon color='secondary'/>
                            </ListItemIcon>
                            <ListItemText primary="Мои клиенты"/>
                        </StyledListItemBtn>
                    </ListItem>
                    <ListItem>
                        <StyledListItemBtn 
                            selected={selectedIdx === 2} 
                            onClick={() => {setComponent("call"); handleItemClick(2)}}
                        >
                            <ListItemIcon>
                                    <WorkOutlineIcon color='secondary'/>
                            </ListItemIcon>
                            <ListItemText primary="Обзвон" />
                        </StyledListItemBtn>
                    </ListItem>
                </List>
            <Divider />
            <EmployeeOptions />
        </Box>
    );
    
    const setContent = () => {
        switch (component) {
            case 'call':
                return <Route path="/dashboard" element={<EmployeeDashboard />} />;
            case 'clients':
                return <Route path="/clients" element={<CollapsibleTable />} />;
            case 'stats':
                return <Route path="/stats" element={<EmployeeStats />} />;
            case 'error':
                return <p>Error</p>;
            default:
                throw new Error('Unexpected process state');
        }
    }

    const elements = useMemo(() => {
        return setContent();
        // eslint-disable-next-line
    }, [component])

      const breadcrumbs = [
        <Link underline="hover" 
            key="1" 
            href="/" 
            sx={{ display: 'flex', alignItems: 'center' }}
        >
            <HomeIcon sx={{ mr: 1 }} fontSize="inherit" />
            Главная
        </Link>,
        <Link
            underline="hover"
            key="2"
            href="/"
        >
            CRM
        </Link>,
        <Typography key="3">
            Обзвон
        </Typography>,
    ];

    return (
        <Box sx={{ display: 'flex' }}>
            <CssBaseline />
            <AppBar 
                position="fixed"         
                sx={{
                    width: { sm: `calc(100% - ${drawerWidth}px)` },
                    ml: { sm: `${drawerWidth}px` },
                    backgroundColor: "#161b22",
                    borderBottom: "1px solid #30363d !important"
                }}>
                <Toolbar>
                    <Typography sx={{flexGrow: 1}} variant="h6" color="textSecondary">TestEmp</Typography>
                    <ProfileMenu sx={{flexGrow: 0}}/>
                </Toolbar>
            </AppBar>
            <Box 
                component="nav"
                sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
            >
                <Drawer variant="temporary">
                    {drawer}
                </Drawer>
                <Drawer
                    variant="permanent"
                    sx={{
                        display: { xs: 'none', sm: 'block' },
                        '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
                    }}
                    open
                >
                    {drawer}
                </Drawer>
            </Box>
            <Box 
                component="main"
                sx={{ flexGrow: 1, p: 2.5, width: { sm: `calc(100% - ${drawerWidth}px)` } }}
            >
                <Toolbar/>
                <Breadcrumbs
                    separator={<NavigateNextIcon fontSize="small" />}
                    sx={{ pb: 1.5, pl: 0.5 }}
                >
                    {breadcrumbs}
                </Breadcrumbs>
                <Routes>
                    {elements}
                </Routes> 
            </Box>
        </Box>
    );
}

export default WorkPage;

错误根源

  1. 导入错误:把BrowserRouter错误重命名为Route,导致返回的<Route>实际是BrowserRouter组件,违反了<Routes>必须直接包含<Route>或<React.Fragment>的规则。
  2. 路由逻辑错误:用useState切换component返回路由的方式不符合React Router v6规范,无法同步浏览器路由状态。

修复步骤

1. 修正React Router导入

替换错误的导入语句:

// 错误写法
import { BrowserRouter as Route, Routes, Router } from "react-router-dom";

// 正确写法
import { Routes, Route, useNavigate, useLocation } from "react-router-dom";

注意:BrowserRouter应该在应用根组件(如App.js)中全局包裹,无需在WorkPage重复导入。

2. 用useNavigate实现路由跳转

删除component状态,侧边栏点击时直接跳转路由并同步选中状态:

const WorkPage = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const [selectedIdx, setSelectedIdx] = useState(2);

  // 点击侧边栏项跳转路由+更新选中索引
  const handleItemClick = (idx, path) => {
    setSelectedIdx(idx);
    navigate(path);
  };

  // 路由变化时自动更新侧边栏选中状态
  useEffect(() => {
    switch(location.pathname) {
      case "/stats": setSelectedIdx(0); break;
      case "/clients": setSelectedIdx(1); break;
      case "/dashboard": setSelectedIdx(2); break;
    }
  }, [location.pathname]);

3. 修正侧边栏点击事件

更新三个侧边栏项的onClick逻辑:

// 统计页面
<StyledListItemBtn 
  selected={selectedIdx === 0} 
  onClick={() => handleItemClick(0, "/stats")}
>
// 客户页面
<StyledListItemBtn 
  selected={selectedIdx === 1} 
  onClick={() => handleItemClick(1, "/clients")}
>
// 呼叫页面
<StyledListItemBtn 
  selected={selectedIdx === 2} 
  onClick={() => handleItemClick(2, "/dashboard")}
>

4. 重构<Routes>配置

删除setContent和useMemo代码,直接在<Routes>中定义所有路由:

<Routes>
  <Route path="/dashboard" element={<EmployeeDashboard />} />
  <Route path="/clients" element={<CollapsibleTable />} />
  <Route path="/stats" element={<EmployeeStats />} />
  <Route path="*" element={<p>Error</p>} />
</Routes>

5. 同步面包屑与路由状态

根据当前路由动态生成面包屑文本:

const getBreadcrumbLabel = () => {
  switch(location.pathname) {
    case "/dashboard": return "Обзвон";
    case "/clients": return "Мои клиенты";
    case "/stats": return "Моя статистика";
    default: return "";
  }
};

const breadcrumbs = [
  <Link underline="hover" 
      key="1" 
      href="/" 
      sx={{ display: 'flex', alignItems: 'center' }}
  >
      <HomeIcon sx={{ mr: 1 }} fontSize="inherit" />
      Главная
  </Link>,
  <Link
      underline="hover"
      key="2"
      href="/dashboard"
  >
      CRM
  </Link>,
  <Typography key="3">
      {getBreadcrumbLabel()}
  </Typography>,
];

核心说明

  • React Router v6强制要求<Routes>的直接子元素只能是<Route>或<React.Fragment>,不能动态返回非Route组件。
  • 路由跳转必须使用useNavigate钩子,才能保持浏览器历史记录与页面状态同步。
  • 全局只需要一个BrowserRouter包裹应用,所有路由组件都要在其范围内。

内容的提问来源于stack exchange,提问作者Богдан Шевченко

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 21:39:36