升级React v18开启StrictMode后组件渲染异常原因排查
该渲染异常和React 18、createRoot()API本身无关,是代码实现不规范叠加React 18严格模式校验逻辑导致的,核心问题有两个:
- 将路由守卫组件
Restricted直接定义在了App组件的函数体内部 - 对React 18严格模式的开发环境重挂载规则、组件协调逻辑不熟悉
为什么React 17下表现符合预期
每次App组件重渲染时,定义在其函数体内部的Restricted会生成全新的函数引用,React会将其识别为完全不同的新组件,直接卸载旧的Restricted实例、重新挂载新实例。React 17的严格模式没有新增组件重挂载校验逻辑,因此刚好能呈现你预期的两次渲染:第一次是初始挂载拉取用户数据前的loading状态,第二次是Redux状态更新触发App重渲染,新的Restricted实例挂载后拿到最新的登录态值。
为什么React 18开启StrictMode后仅渲染一次
React 18开发环境下,严格模式会自动执行一次「组件挂载-卸载-重挂载」流程,用来提前暴露不规范的副作用写法。嵌套定义Restricted的写法在这个逻辑下会出现协调异常:
- 首次挂载时
App渲染,生成第一版Restricted组件,渲染loading状态 - 严格模式触发第一次卸载+重挂载,此时
App本身还未因Redux状态更新触发重渲染,Restricted引用暂时不变,重挂载后仍然拿到初始的loggedIn = null值 - 拉取用户数据的
useEffect执行完成,dispatch更新Redux状态,loggedIn变化触发App重渲染,此时生成的是引用完全不同的新版Restricted组件,React在协调路由元素时会直接丢弃旧的Restricted实例,不会触发你预期的状态更新后重渲染流程,最终表现为组件仅渲染一次、卡在初始状态。
1. 将Restricted组件移出App的定义范围
永远不要在父组件函数体内部定义子组件,这种写法会导致每次父组件重渲染时,子组件都被强制卸载重挂载,既浪费性能,也会引发状态丢失、渲染逻辑异常等问题。
将Restricted挪到App外部,依赖的路由、Redux状态直接在组件内部通过hooks获取即可:
import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { useSelector } from 'react-redux'; // 组件定义在顶层,引用稳定不会随App重渲染变化 const Restricted = ({ routeType }) => { const location = useLocation(); // 按实际项目的Redux state路径取loggedIn值 const { loggedIn } = useSelector(state => state.user); const cartSelection = location.state?.cartSelection; if(loggedIn !== null) { if(routeType === "guest") { if(loggedIn){ if(location.state?.from) { return <Navigate replace to={location.state.from} state={{cartSelection: cartSelection}} /> } return <Navigate replace to="/" /> } return <Outlet />; } else if(routeType === "user") { return loggedIn ? <Outlet /> : <Navigate replace to="/" />; } } return <h2>Loading user data...</h2> } function App() { // 原有App逻辑,删除嵌套在内部的Restricted定义 useEffect(() => { let isSubscribed = true; // 异步拉取数据 fetchUserData().then(res => { // 仅在组件未卸载时更新状态 if(isSubscribed) { // dispatch更新Redux } }) return () => { isSubscribed = false } }, [dispatch, gettingUserInfo]); return ( <> <Navbar /> <Routes location={location} key={location.pathname}> {/* 直接使用顶层定义的Restricted即可 */} <Route element={<Restricted routeType="user" />}> <Route path="/profile" element={<Profile/>} /> </Route> <Route element={<Restricted routeType="guest" />}> <Route path="/login" element={<Login/>} /> </Route> </Routes> </> ); }
2. 修正useEffect逻辑
原代码中isSubscribed的判断位置无效:effect刚执行时isSubscribed永远为初始值true,这个标记的作用是在组件卸载时阻止过期的异步请求更新状态,需要把判断逻辑写在异步请求的回调中,参考上面代码块中的写法即可。
补充说明
React 18严格模式下开发环境的双挂载行为是刻意设计的校验逻辑,不是bug,目的就是提前暴露类似「组件嵌套定义」「副作用缺少清理逻辑」这类问题,生产环境不会触发双挂载,不需要手动关闭StrictMode。
内容的提问来源于stack exchange,提问作者Monstar

