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
相关产品推荐
相关产品推荐

