如何正确卸载使用createRoot创建的React根实例?
问题重现
我有一个组件,内部频繁调用React 18的createRoot渲染内容。在componentWillUnmount中调用unmount卸载时,会触发以下警告:
Warning: Attempted to synchronously unmount a root while React was already rendering. React cannot finish unmounting the root until the current render has completed, which may lead to a race condition.
该代码在React 17使用ReactDOM.render()时无此警告,最小复现代码如下:
import React from "react"; import { createRoot } from "react-dom/client"; class Example extends React.Component { componentWillUnmount() { if (this._root) { // 触发警告的代码 this._root.unmount(); } } render() { return ( <div ref={(el) => { if (el) { console.log("creating root"); this._root = createRoot(el); this._root.render(<div>rendered with createRoot</div>); } }} /> ); } } class Example2 extends React.Component { render() { // 触发重渲染的示例代码 return ( <div> {this.state?.aa ? <div>bbb</div> : <Example />} <button onClick={() => this.setState({ aa: true })}>click me</button> </div> ); } } createRoot(document.getElementById("app")).render(<Example2 />);
原因分析
React 18引入了并发渲染机制,createRoot创建的根节点卸载操作unmount是异步执行的逻辑。而componentWillUnmount属于React同步渲染流程中的生命周期钩子,此时直接调用unmount会导致渲染流程与卸载操作冲突,触发竞态条件警告。React 17的ReactDOM.render没有并发渲染机制,因此不会出现该问题。
解决方案
方法1:用setTimeout延迟卸载
将unmount放入宏任务队列,避开当前同步渲染流程:
componentWillUnmount() { if (this._root) { setTimeout(() => { this._root.unmount(); this._root = null; }, 0); } }
方法2:改用函数组件+useEffect清理(推荐)
将类组件转为函数组件后,利用useEffect的清理函数处理卸载——该清理逻辑会在渲染完成后异步执行,不会与当前渲染流程冲突:
import React, { useEffect, useRef } from "react"; import { createRoot } from "react-dom/client"; const Example = () => { const containerRef = useRef(null); const rootRef = useRef(null); useEffect(() => { if (containerRef.current) { rootRef.current = createRoot(containerRef.current); rootRef.current.render(<div>rendered with createRoot</div>); } return () => { if (rootRef.current) { rootRef.current.unmount(); } }; }, []); return <div ref={containerRef} />; };
方法3:使用ReactDOM.unmountComponentAtNode兼容写法
如果需要同时兼容React 17和18,可使用该方法替代root.unmount(),它在React 18中能正常工作且不会触发警告:
import ReactDOM from 'react-dom'; componentWillUnmount() { if (this._root && this._root._internalRoot?.containerInfo) { ReactDOM.unmountComponentAtNode(this._root._internalRoot.containerInfo); this._root = null; } }
注意:_internalRoot是React内部属性,后续版本可能变动,需谨慎使用。
内容的提问来源于stack exchange,提问作者sydd

