React Router传递子路由致孙组件重复挂载的原因与非Context解法
一、为什么只有ThirdPage会重复挂载?
咱们直接抓核心:ChildRoutes函数的引用每次渲染都在变,这是导致孙组件反复挂载的罪魁祸首。
看你的Routing组件代码,每次它渲染时,都会给上层组件(比如MainPage)传递一个全新的ChildRoutes函数:
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
当MainPage的foo状态变化时,MainPage会重新渲染,这时它传给SecondPage的ChildRoutes已经是一个新的函数引用了。
那为什么SecondPage自己没事?因为SecondPage是被Route组件用component prop直接渲染的,component的值始终是SecondPage这个固定的组件引用——React只要组件类型没变,就会复用现有实例,只更新props,不会卸载重挂载。
但ThirdPage的情况不一样:SecondPage接收到新的ChildRoutes后会重新渲染,此时<ChildRoutes foo={foo} bar={bar} />相当于调用了一个全新的函数组件。React会认为这是完全不同的组件类型,于是直接卸载原来的ThirdPage,再重新挂载新的实例,所以你看到的mount third日志会每次都触发。
二、不用Context的解决办法
方法1:用useCallback缓存ChildRoutes函数
最直接的方式是让ChildRoutes的引用稳定下来,只有当它的依赖项变化时才更新。我们可以在Routing组件里用useCallback来缓存这个函数:
修改后的Routing组件代码:
import React, { useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; const Routing = ({ routes: passedRoutes, ...rest }) => { if (!passedRoutes) return null; // 用useCallback缓存函数,依赖项只有passedRoutes(你的路由配置是固定的,所以这个依赖不会变) const stableChildRoutes = useCallback((props) => { return <Routing routes={passedRoutes} {...props} />; }, [passedRoutes]); return ( <Switch> {passedRoutes.map(({ routes, component: Component, ...route }) => { return ( <Route key={route.path} {...route}> <Component {...rest} ChildRoutes={stableChildRoutes} /> </Route> ); })} </Switch> ); };
这样一来,只要你的路由配置passedRoutes不变,stableChildRoutes的引用就会一直稳定。当MainPage状态变化时,传给SecondPage的ChildRoutes还是同一个函数,SecondPage重新渲染时,孙组件的路由组件不会被视为新组件,ThirdPage也就不会反复挂载了。
方法2:把路由配置设为组件静态属性
如果你的路由结构比较固定,可以把每个组件的子路由定义成它的静态属性,避免通过props传递函数:
// 给每个页面组件添加静态routes属性 MainPage.routes = [ { path: '/main/second', component: SecondPage, routes: [ { path: '/main/second/third', component: ThirdPage }, ] } ]; SecondPage.routes = [ { path: '/main/second/third', component: ThirdPage }, ]; // 修改Routing组件,直接读取组件的静态routes const Routing = ({ routes: passedRoutes, ...rest }) => { if (!passedRoutes) return null; return ( <Switch> {passedRoutes.map(({ component: Component, ...route }) => { return ( <Route key={route.path} {...route}> <Component {...rest} /> {/* 直接渲染当前组件的子路由 */} <Routing routes={Component.routes} {...rest} /> </Route> ); })} </Switch> ); };
这种方式彻底避免了传递动态函数,自然不会有引用变化的问题,但缺点是路由结构的灵活性会降低,适合路由固定的场景。
总结
本质问题就是动态创建的函数引用不稳定,导致React误判组件类型,触发卸载重挂载。用useCallback缓存函数是最灵活的非Context方案,既能保留你原来的路由结构设计,又能解决重复挂载的问题。
内容的提问来源于stack exchange,提问作者Kevin

