内联引用清理在并发的过渡回滚期间抛出TypeError:无法读取null的属性
我最近将我们的应用升级到React 19,并开始将遗留的 useEffect DOM清理工作重构为使用新的Ref Cleanup Function功能(回调refs可以返回清理函数)。
重构在应用的95% 场景下工作正常,但我在一个使用 useDeferredValue 和条件渲染的仪表盘组件中遇到了一个莫名其妙的崩溃。
下面是导致问题的组件的简化版本。它基于一个延迟查询,在一个容器元素中动态渲染一个第三方图表引擎:
import { useState, useDeferredValue } from 'react';
import { ExternalChartEngine } from 'awesome-charts';
export default function Dashboard({ searchTerms }) {
const [showMetrics, setShowMetrics] = useState(true);
// Deferring the search terms to keep the UI responsive
const deferredSearch = useDeferredValue(searchTerms);
if (!showMetrics) return <div>Metrics Hidden</div>;
return (
<div className="dashboard-container">
<div
ref={(element) => {
if (!element) return;
// Initialize the third-party chart
const chartInstance = ExternalChartEngine.init(element, {
query: deferredSearch
});
// React 19 Cleanup function
return () => {
if (chartInstance) {
chartInstance.destroy();
}
};
}}
/>
</div>
);
}
每当 searchTerms 快速变化(触发延迟过渡)且用户同时切换 setShowMetrics(false) 时,应用会在React的 reconciler内完全崩溃,控制台出现如下错误:
Uncaught TypeError: Cannot read properties of null (reading 'destroy') at safelyCallDestroy (react-dom.development.js:22943) at commitDeletionEffectsOnFiber (react-dom.development.js:24210) at recursivelyTraverseDeletionEffects (react-dom.development.js:24120)
我已经尝试过的办法
- 如果我把ref改回一个标准的
useRef,并在一个标准的useEffect中处理初始化/清理,崩溃就会消失。 - 如果我移除
useDeferredValue,崩溃也会消失。
为什么在延迟渲染过渡期间,从内联回调ref返回清理函数会失败?
解决方案
我怀疑这里的问题在于你使用了一个内联的ref函数,它在每个渲染周期都会被创建/重新创建(如果把应用渲染到 StrictMode 组件中,情况可能更糟,请参阅 在开发模式下通过重新运行ref回调来修复的bug)。它被调用的次数比你预期的要多,而在随后的调用中,chartInstance 已经被销毁/置空。
请将其重构为使用 useCallback 来提供一个稳定的引用。
import { useCallback, useDeferredValue, useState } from 'react';
import { ExternalChartEngine } from 'awesome-charts';
export default function Dashboard({ searchTerms }) {
const [showMetrics, setShowMetrics] = useState(true);
// Deferring the search terms to keep the UI responsive
const deferredSearch = useDeferredValue(searchTerms);
const refHandler = useCallback((element) => {
if (!element) return;
// Initialize the third-party chart
const chartInstance = ExternalChartEngine.init(element, {
query: deferredSearch
});
// React 19 Cleanup function
return () => {
if (chartInstance) {
chartInstance.destroy();
}
};
}, [deferredSearch]);
if (!showMetrics) return <div>Metrics Hidden</div>;
return (
<div className="dashboard-container">
<div ref={refHandler} />
</div>
);
}