内联引用清理在并发的过渡回滚期间抛出TypeError:无法读取null的属性

前端开发 2026-07-09

我最近将我们的应用升级到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)

我已经尝试过的办法

  1. 如果我把ref改回一个标准的 useRef,并在一个标准的 useEffect 中处理初始化/清理,崩溃就会消失。
  2. 如果我移除 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>
  );
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章