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

React嵌套路由导致含空依赖数组的useEffect重复执行问题

React嵌套路由组件重挂载问题解决

问题说明

在React项目中实现嵌套路由时,遇到以下问题:

  • 带有嵌套路由的Patient父组件,首次通过侧边栏的Link点击打开患者详情页时,父组件会被强制重新挂载,导致仅需挂载时执行的useEffect(调用getAllPatients方法)重复触发。
  • 后续切换其他患者时,父组件不会重挂载,功能正常。
  • 调整useEffect的依赖数组无效,因为根源是组件重挂载。

相关代码

Patient组件代码

import React, { useState, useEffect } from 'react';
import { Row, Col } from 'reactstrap';
import { Route, Switch, useLocation, useRouteMatch } from 'react-router-dom';

//components
import PatientSidebar from './PatientSidebar';
import PatientData from './PatientData';

//redux
import { useDispatch } from 'react-redux';
import { getAllPatients } from '../../store/actions';

const Patient = () => {
  const dispatch = useDispatch();
  const location = useLocation();
  let { path, url } = useRouteMatch(); 
  console.log(location);
  const [barWidth, setBarWidth] = useState();
  const [prevent, setPrevent] = useState('');


  /*This useEffect run when component is first mounted and it again runs when i select patient from sidebar (code is given below) to open its data using Link and nested routing*/
  useEffect(() => {
    dispatch(getAllPatients());
  }, [dispatch]);

  document.title = 'Patients';
  return (
    <React.Fragment>
      <div
        className='page-content ps-0 ps-md-3'
        style={{
          paddingLeft: '0',
          paddingTop: '70px',
          paddingRight: '0',
          overflow: 'hidden',
        }}
      >
        <Row>
          <Col
            xs={12}
            lg={3}
            style={{ paddingRight: '0', width: `${barWidth}px` }}
            className='position-relative toggle-patient-bar ps-0'
          >
            <PatientSidebar setBarWidth={setBarWidth} />
          </Col>
          <Switch>
            <Route
              render={() => <PatientData barWidth={barWidth}/>}
              exact
              path={`/patient/:id`}
              strict
            />
          </Switch>
        </Row>
      </div>
    </React.Fragment>
  );
};

export default Patient;

侧边栏组件代码

import React, { useEffect, useState, useRef } from 'react';
import { Button, Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap';
import { Link, useRouteMatch } from 'react-router-dom';
import classnames from 'classnames';

//redux
import { useSelector, useDispatch } from 'react-redux';
import { showPatient, clearBills, loadMorePatients } from '../../store/actions';

//view hook
import useView from '../../Components/Hooks/UseView';

//loader
import Loader from '../../Components/Common/Loader';
import LoadMore from '../../Components/Common/LoadMore';

const PatientSidebar = ({ setBarWidth }) => {
  let { path, url } = useRouteMatch();
  //search name
  const patient = useSelector((state) => state.viewPatient.patient);
  //get all patients
  const patients = useSelector((state) => state.getPatients.patients);
  //call redux action
  const dispatch = useDispatch();
  // Custom Tabs Bordered
  const [tHeight, setTHeight] = useState();
  const [customActiveTab, setcustomActiveTab] = useState('1');

  const toggleCustom = (tab) => {
    if (customActiveTab !== tab) {
      setcustomActiveTab(tab);
    }
  };

  const bar = useRef();
  const topRef = useRef();
  const headRef = useRef();
  const navsRef = useRef();
  useEffect(() => {
    if (window.innerWidth >= 1024) setBarWidth(bar.current.clientWidth);
    setTHeight(
      parseInt(topRef.current.clientHeight) +
        parseInt(headRef.current.clientHeight) +
        parseInt(navsRef.current.clientHeight)
    );
  }, [window.innerWidth]);

  const toogleTab = () => {
    var windowSize = document.documentElement.clientWidth;
    if (windowSize < 1300) {
      document.documentElement.getAttribute('toogle-patient-tab') === 'sm'
        ? document.documentElement.setAttribute('toogle-patient-tab', 'lg')
        : document.documentElement.setAttribute('toogle-patient-tab', 'sm');
    }
  };

  const getPatient = (item) => {
    if (patient && patient._id !== item._id) {
      dispatch(clearBills());
      dispatch(showPatient(item));
    } else {
      dispatch(showPatient(item));
    }
  };

  return (
    <React.Fragment>
      <div
        ref={bar}
        className='patient-sidebar fancy-bar p-2 pt-3 position-fixed'
      >
        <div
          ref={topRef}
          className='d-flex justify-content-between align-items-center mt-2 mt-xl-0'
        >
          <h5 className='font-size-25 font-thin margin-bottom-0'>Patients</h5>
          <div className='d-flex'>
            <Button className='font-size-14 h-auto font-thin btn-primary btn-sm p-2 pt-1 pb-1'>
              Learn
            </Button>
            <div className='d-flex patient-menu-btn'>
              <button
                onClick={toogleTab}
                style={{ height: 'auto' }}
                type='button'
                className='btn btn-sm px-3 fs-16 header-item topnav-hamburger'
                id='topnav-hamburger-icon'
              >
                <span className='hamburger-icon'>
                  <span className='bg-dark'></span>
                  <span className='bg-dark'></span>
                  <span className='bg-dark'></span>
                </span>
              </button>
            </div>
          </div>
        </div>
        <div className='toogle-patient-tab'>
          <h6 ref={headRef} className='font-size-14 font-thin mt-3'>
            Jagruti Rehabilitation Center - Taloja
          </h6>
          <div className='mt-3'>
            <TabContent
              activeTab={customActiveTab}
              className='text-muted'
            >
              <TabPane tabId='1' id='home1'>
                <div className='d-flex'>
                  <div className='flex-grow-1 ms-0'>
                    {(patients || []).map((item, idx) => {
                      return (
                        <Link
                          to={`/patient/${item._id}`}
                          className='text-decoration-none text-secondary font-size-16'
                          key={item._id}
                        >
                          <div
                            key={idx}
                            style={{
                              borderColor:
                                patient && patient._id === item._id
                                  ? '#1e90ff'
                                  : '',
                            }}
                            className='p-2 patients border-bottom-1 d-flex align-items-center'
                            onClick={() => {
                              getPatient(item);
                              toogleTab();
                            }}
                          >
                            <img
                              className='rounded-circle avatar-xxs header-profile-user me-3 mt-0'
                              src={item.avatar}
                              alt='Patient Avatar'
                            />
                            <h3 className='text-decoration-none text-capitalize mb-0 text-secondary font-size-14'>
                              {item.firstName}
                            </h3>
                          </div>
                        </Link>
                      );
                    })}
                    {!patients && (
                      <div className='text-center'>
                        <div
                          className='spinner-border text-primary me-3'
                          role='status'
                        >
                          <span className='visually-hidden'>Loading...</span>
                        </div>
                      </div>
                    )}
                    {patients && <LoadMore patients={patients} />}
                  </div>
                </div>
              </TabPane>
              <TabPane tabId='2'>
                <div className='d-flex'>
                  <div className='flex-grow-1 ms-0'></div>
                </div>
              </TabPane>
              <TabPane tabId='3'>
                <div className='d-flex'>
                  <div className='flex-grow-1 ms-0'></div>
                </div>
              </TabPane>
            </TabContent>
          </div>
        </div>
      </div>
    </React.Fragment>
  );
};

export default PatientSidebar;

全局路由配置代码

import React, { lazy } from 'react';
import { Redirect } from 'react-router-dom';

//Dashboard
const Dashboard = lazy(() => import('../pages/Dashboard'));

//Client
const ViewPatients = lazy(() => import('../pages/ViewPatients/ViewPatients'));
const EditPatient = lazy(() => import('../pages/EditPatient/EditPatient'));
const AddUser = lazy(() => import('../pages/User/AddUser'));
const ViewAllUsers = lazy(() => import('../pages/User/ViewUsers'));
const EditUser = lazy(() => import('../pages/User/EditUser'));
const UsersLog = lazy(() => import('../pages/ActivityLog/UsersLog'));
const PatientsLog = lazy(() => import('../pages/ActivityLog/PatientsLog'));
const PatientLog = lazy(() => import('../pages/Patient/PatientLog'));
const ViewPatient = lazy(() =>
  import('../pages/Patient/ViewPatient/ViewPatient')
);
const Patient = lazy(() => import('../pages/Patient/Patient'));

//Reports
const Reports = lazy(() => import('../pages/Reports/Reports'));

//Quick Registration
const QuickRegistration = lazy(() =>
  import('../pages/QuickRegistration/QuickRegistration')
);
const DetailRegistration = lazy(() =>
  import('../pages/DetailRegistration/DetailRegistration')
);

//Authentication Inner pages
const ResetPassword = lazy(() =>
  import('../pages/ResetPassword/ResetPassword')
);

//login
const Login = lazy(() => import('../pages/Authentication/Login'));
const ForgetPassword = lazy(() =>
  import('../pages/Authentication/ForgetPassword')
);
const Logout = lazy(() => import('../pages/Authentication/Logout'));
const Register = lazy(() => import('../pages/Authentication/Register'));

// User Profile
const UserProfile = lazy(() => import('../pages/Authentication/user-profile'));
const Profile = lazy(() => import('../pages/Profile/Profile'));

// Settings
const AdvancePaymentSettings = lazy(() =>
  import('../pages/AdvancePaymentSettings/AdvancePaymentSettings')
);
const InvoiceSettings = lazy(() =>
  import('../pages/InvoiceSettings/InvoiceSettings')
);
const Settings = lazy(() => import('../pages/Settings/Setting'));

const authProtectedRoutes = [
  { path: '/dashboard', component: Dashboard },
  { path: '/index', component: Dashboard },

  //Client
  { path: '/view-patients', component: ViewPatients },
  { path: '/edit-patient', component: EditPatient },
  { path: '/add-user', component: AddUser },
  { path: '/view-users', component: ViewAllUsers },
  { path: '/edit-user', component: EditUser },
  { path: '/users-log', component: UsersLog },
  { path: '/patient-log', component: PatientLog },
  { path: '/user-profile', component: Profile },
  //page where this issues occures -------------- start
  { path: '/patient', component: Patient },
  { path: '/patient/:id', component: Patient },
  //page where this issues occures -------------- end
  { path: '/quick-registration', component: QuickRegistration },
  { path: '/detail-registration', component: DetailRegistration },
  { path: '/settings', component: Settings },
  { path: '/advance-payment-settings', component: AdvancePaymentSettings },
  { path: '/invoice-settings', component: InvoiceSettings },
  { path: '/reports', component: Reports },
  { path: '/reset-password', component: ResetPassword },

  {
    path: '/',
    exact: true,
    component: () => <Redirect to='/login' />,
  },
];

const publicRoutes = [
  // Authentication Page
  { path: '/logout', component: Logout },
  { path: '/login', component: Login },
  { path: '/forgot-password', component: ForgetPassword },
  { path: '/register', component: Register },
];

export { authProtectedRoutes, publicRoutes };

根路由文件代码

import React, { useEffect, Suspense } from 'react';
import { Switch, Route } from 'react-router-dom';

//Layouts
import NonAuthLayout from '../Layouts/NonAuthLayout';
import VerticalLayout from '../Layouts/index';
//routes
import { authProtectedRoutes, publicRoutes } from './allRoutes';
import { AuthProtected, AccessRoute } from './AuthProtected';

//load user
import { loadUser } from '../store/auth/actions/auth';
import { useDispatch } from 'react-redux';
import setAuthToken from '../utils/setAuthToken';

//import alerts
import Alerts from '../Components/Common/Alerts';

//loader
import Loader from '../Components/Common/LargeLoader';

if (localStorage.token) {
  setAuthToken(localStorage.token);
}

const Index = () => {
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(loadUser());
  }, []);

  const availablePublicRoutesPaths = publicRoutes.map((r) => r.path);
  const availableAuthRoutesPath = authProtectedRoutes.map((r) => r.path);
  return (
    <React.Fragment>
      <Alerts />
      <Suspense fallback={<Loader />}>
        <Switch>
          <Route path={availablePublicRoutesPaths}>
            <NonAuthLayout>
              <Switch>
                {publicRoutes.map((route, idx) => (
                  <Route
                    path={route.path}
                    component={route.component}
                    key={idx}
                    exact={true}
                  />
                ))}
              </Switch>
            </NonAuthLayout>
          </Route>

          <Route path={availableAuthRoutesPath}>
            <VerticalLayout>
              <Switch>
                {authProtectedRoutes.map((route, idx) => (
                  <AccessRoute
                    path={route.path}
                    component={route.component}
                    key={idx}
                    exact={true}
                  />
                ))}
              </Switch>
            </VerticalLayout>
          </Route>
        </Switch>
      </Suspense>
    </React.Fragment>
  );
};

export default Index;

问题根源

全局路由配置中同时注册了/patient和/patient/:id两个指向Patient组件的路由。首次从侧边栏点击跳转到/patient/:id时,React Router会匹配到/patient/:id路由,导致原本挂载的/patient对应的Patient组件被卸载,随后重新挂载/patient/:id对应的Patient组件,因此触发了挂载时的useEffect。而后续切换不同患者的/patient/:id路径时,路由匹配的是同一个/patient/:id路由,组件仅更新props,不会重挂载。

解决方案

步骤1:修正全局路由配置

删除全局路由中的{ path: '/patient/:id', component: Patient },仅保留父路由:

// 保留这个
{ path: '/patient', component: Patient },
// 删除这一行
// { path: '/patient/:id', component: Patient },

步骤2:调整Patient组件内的嵌套路由

使用useRouteMatch返回的path来动态定义子路由路径,替代硬编码的/patient/:id,同时移除exact和strict属性(父路由已负责匹配/patient前缀):

<Switch>
  <Route
    render={() => <PatientData barWidth={barWidth}/>}
    path={`${path}/:id`}
  />
</Switch>

步骤3:优化useEffect(可选)

为了确保getAllPatients仅在组件首次挂载时执行,可以添加一个状态标记来防止重复调用:

const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
  if (!isMounted) {
    dispatch(getAllPatients());
    setIsMounted(true);
  }
}, [dispatch, isMounted]);

不过完成前两步修正后,组件不会再重挂载,此步骤可以省略。

验证效果

修改完成后,首次点击侧边栏的患者链接时,Patient父组件不会被重挂载,getAllPatients只会执行一次;后续切换患者时,仅子组件PatientData更新,父组件状态保持稳定。

内容的提问来源于stack exchange,提问作者Owais Ahmed

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 03:00:56