React.memo优化两处条件渲染Banner路由切换闪烁失效问题
React.memo 对这个场景完全没用——它只能优化同一个组件实例的重渲染,管不了组件卸载再挂载的场景。
你现在的写法里,两个分支的<ExampleBanner />在虚拟DOM树里的位置完全不一样:非Home页时它是Navigator的直接子节点,Home页时它是AnotherComponent内部的子节点。路由切换时React对比虚拟DOM,会直接判定这是两个完全无关的组件,直接把旧的卸载、新的挂载,整个组件生命周期从头走一遍,重挂载的过程自然会引发闪烁。你观察到的单方向缓存生效只是React diff的巧合,不是memo真的在跨位置缓存组件。
方案1(优先选,最简单):把Banner提到稳定的公共父层
既然Banner渲染内容固定、始终展示在页面顶部,根本没必要拆成两个条件渲染的节点。直接把它挪到不会随路由切换卸载的公共父层级,让它全程只挂载一次,从根源上消除重挂:
{/* 放在Navigator外层/内层的稳定位置,永远不做条件卸载 */} <ExampleBanner /> <Navigator> <AnotherComponent /> </Navigator>
如果有DOM结构的要求——比如非Home页时Banner要在Navigator的DOM容器里、Home页时要在AnotherComponent的容器里,直接用CSS把这个固定挂载的Banner定位到对应视觉位置就行,比反复挂载组件性能好得多,完全不会闪。
方案2(强DOM结构要求时用):单实例+Portal传送
如果业务强制要求Banner必须出现在对应组件的DOM树中(比如要继承父容器的上下文、样式层级),不要写两个条件渲染的Banner,只保留一个始终挂载的Banner实例,用React的Portal把它的DOM节点传送到目标位置:
import { createPortal } from 'react-dom' // 在两个组件的公共父层维护目标容器 const [bannerTarget, setBannerTarget] = useState(null) useEffect(() => { // 路由切换时切换要挂载的DOM插槽 const targetId = route.name === 'Home' ? 'home-banner-slot' : 'nav-banner-slot' setBannerTarget(document.getElementById(targetId)) }, [route.name]) return ( <> <Navigator> {/* 非Home页时在Navigator里留插槽 */} {route.name !== 'Home' && <div id="nav-banner-slot" />} <AnotherComponent /> </Navigator> {/* Banner全程只挂载这一次,永远不会被卸载 */} {bannerTarget && createPortal(<ExampleBanner />, bannerTarget)} </> ) // AnotherComponent里只需要留对应插槽 const AnotherComponent = () => { return route.name === 'Home' ? <div id="home-banner-slot" /> : null }
这种写法Banner组件全程只走一次挂载流程,哪怕不用React.memo包裹也不会有重挂导致的闪烁。
注意:别尝试给两个位置的Banner加相同key实现复用,React不会跨父层级移动复用组件实例,相同key也没用,还是会触发卸载重挂。
内容的提问来源于stack exchange,提问作者arun2567

