React18中state与ref存储组件时调用方式为何存在差异
两种组件调用方式的差异说明
你在异步组件封装中,分别将动态加载的组件存入state和ref,用两种不同方式渲染都能得到结果,相关实现代码如下:
export default function getAsyncComponent(load) { const AsyncComponent = () => { const [component, setComponent] = useState(null) const com = useRef(null) const loadComponent = async () => { const _component = await load() com.current = _component.default setComponent(_component.default) } useEffect(() => { loadComponent() }, []) console.log(component, com.current) const Com = com.current return ( <div> // Both ways are works. {component ? component : null} {Com ? <Com /> : null} </div> ) } return <AsyncComponent /> }
控制台打印出两种完全不同的存储值:
// component的打印结果 {$$typeof: Symbol(react.element), type: 'div', key: null, ref: null, props: {…}, …} // com.current的打印结果 ƒ IndexPage() { return ((0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)("div", { children: [(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("img", { className: _index_less__WEBPACK_I…
该方法的使用场景为React Router 6的路由配置:
function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={getAsyncComponent(() => import('src/pages/index'))} /> </Routes> </BrowserRouter> ) }
依赖版本为react@18.1、react-router-dom@6.3。
从打印结果就能看出两种写法的本质区别:
- 存入state的
component最终变成了已经实例化完成的React元素对象 - 存入ref的
com.current始终是从动态import拿到的原始组件函数引用
两种写法能同时"看起来正常工作"只是表面现象,底层运行逻辑和可靠性差异极大:
1. ref存储 + <Com /> 渲染:符合React规则的正确写法
你在ref里存的始终是未执行的原始组件函数,使用<Com />是标准JSX语法:
- JSX识别到大写开头的变量会将其作为组件处理,按正常流程调用组件函数、初始化hooks、挂载生命周期、生成React元素插入渲染树
- 组件状态、上下文、props传递都完全符合React预期,不会出现额外重渲染、重复挂载的问题
- 这也是所有React组件渲染的标准推荐写法
2. state存储 + 直接插值{component}渲染:存在隐患的错误写法
你把原始组件函数存入state后直接放在插值位置渲染,本身就违反React的渲染规则:
- 正常情况下,JSX插值位置只接受React元素、字符串、数字、布尔值、null/undefined,如果传入函数React会直接抛出
Functions are not valid as a React child的警告,不会正常渲染。你这里能看到渲染结果只是React 18版本下的特殊兼容巧合,后续版本升级后随时可能直接失效。 - 从你的控制台输出就能看出异常:你存入state的明明是组件函数,打印出来却变成了React元素——这说明渲染流程已经出现了非预期的执行:组件函数被提前意外调用,生成的元素脱离了React的生命周期管控,会导致hooks重复执行、状态丢失、上下文失效、重复挂载等很难排查的问题。
- 这种写法还可能触发无限重渲染、内存泄漏等隐性bug。
额外说明
你当前实现的异步组件逻辑本身也存在缺陷:
- 没有处理加载失败、加载中占位的逻辑
- 没有做加载结果缓存,重复调用会重复执行动态import
- 针对React Router 6的路由懒加载场景,官方推荐直接使用
React.lazy搭配Suspense实现,不需要自己手写封装。
内容的提问来源于stack exchange,提问作者ZGZ
相关产品推荐
相关产品推荐

