为什么react-router Route的render属性触发组件重挂载而非重渲染?
问题根因
- 列表渲染缺少稳定唯一key:你在
MySwitch的Routerender方法最后对缓存路由做列表渲染时,没有给每个外层div添加key属性。React Diff列表类子元素时,如果没有稳定key会默认按位置比对,上层组件重渲染触发render重新执行时,会判定所有列表元素为新节点,直接销毁旧节点重新挂载,导致缓存组件每次都被重新初始化。 - 全局缓存变量污染:你把
cacheRouteMap声明为组件外的全局变量,会出现多MySwitch实例缓存冲突、组件卸载后缓存无法自动清理的问题,也可能引发异常挂载。 - 路由组件定义位置错误:如果你的路由组件(比如测试代码里的
Demo)定义在父组件内部,父组件重渲染时会生成新的组件引用,React会判定为不同类型组件,直接触发重挂载。 - Route props 写法错误:你原
App代码里给Route传的component属性值是JSX元素,不符合React Router v5的规范,component属性应该接收组件构造函数,传JSX会导致每次渲染生成新元素,触发重挂载。
修改方案
1. 修正MySwitch实现
// MySwitch import React from 'react' import { Route, matchPath } from 'react-router-dom' const MySwitch = props => { // 缓存放入组件内部ref,避免全局污染 const cacheRouteMap = React.useRef({}) return ( <Route path="*" render={context => { const location = props.location || context.location let element let match let currentMatchPath React.Children.forEach(props.children, child => { // eslint-disable-next-line no-eq-null if (match == null && React.isValidElement(child)) { element = child const path = child.props.path || child.props.from currentMatchPath = path match = path ? matchPath(location.pathname, {...child.props, path}) : context.match } }) if (currentMatchPath) { if (!cacheRouteMap.current[currentMatchPath]) { // 首次匹配缓存组件 cacheRouteMap.current[currentMatchPath] = React.cloneElement(element, { location, computedMatch: match, }) } else { // 已缓存的组件更新路由相关props,避免拿到旧的路由数据 cacheRouteMap.current[currentMatchPath] = React.cloneElement(cacheRouteMap.current[currentMatchPath], { location, computedMatch: match, }) } } // 给每个列表项添加唯一key,值为路由路径 return Object.entries(cacheRouteMap.current).map(([path, cachedElement]) => { return ( <div key={path} style={{display: path === currentMatchPath ? 'block' : 'none'}} > {cachedElement} </div> ) }) }} /> ) }
2. 修正路由写法
App里的Route组件不要给component传JSX元素,两种正确写法二选一:
// 写法1:用component传组件构造函数 <Route path="path1" component={Component1}> // 写法2:用render返回JSX <Route path="path1" render={() => <Component1 />}>
3. 修正组件定义位置
所有路由组件要定义在父组件外部,不要嵌套在其他组件的函数体内。如果必须在render函数内返回组件,可以用useCallback包裹render回调,避免每次生成新的函数引用:
const App = () => { const [visible, setVisible] = useState(false) // 用useCallback缓存render回调 const renderDemo = React.useCallback(() => <Demo />, []) return ( <div> <div onClick={() => setVisible(!visible)}>button</div> <div style={{display: visible ? 'block' : 'none'}}>xxxxx</div> <Router> <Switch> <Route path="/" render={renderDemo} /> <Redirect to="/" /> </Switch> </Router> </div> ) }
修改完成后上层组件重渲染时,缓存的路由组件只会触发重渲染,不会重新挂载。
内容的提问来源于stack exchange,提问作者Sky Clong
相关产品推荐
相关产品推荐

