You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级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的写法在这个逻辑下会出现协调异常:

  1. 首次挂载时App渲染,生成第一版Restricted组件,渲染loading状态
  2. 严格模式触发第一次卸载+重挂载,此时App本身还未因Redux状态更新触发重渲染,Restricted引用暂时不变,重挂载后仍然拿到初始的loggedIn = null值
  3. 拉取用户数据的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 12:51:20