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

React 19内联Ref清理函数在延迟渲染过渡中触发TypeError问题

React 19回调Ref清理函数在useDeferredValue+条件渲染场景崩溃原因解析

近期将应用升级至React 19,使用新的回调Ref返回清理函数特性重构了原有的useEffect DOM清理逻辑,大部分场景运行正常,但在结合useDeferredValue和条件渲染的仪表盘组件中出现崩溃问题。

问题代码

import { useState, useDeferredValue } from 'react';
import { ExternalChartEngine } from 'awesome-charts'; 

export default function Dashboard({ searchTerms }) {
  const [showMetrics, setShowMetrics] = useState(true);
  // 延迟搜索词更新以保持UI响应
  const deferredSearch = useDeferredValue(searchTerms);

  if (!showMetrics) return <div>Metrics Hidden</div>;

  return (
    <div className="dashboard-container">
      <div 
        ref={(element) => {
          if (!element) return;
          
          // 初始化第三方图表
          const chartInstance = ExternalChartEngine.init(element, {
            query: deferredSearch
          });

          // React 19清理函数
          return () => {
            if (chartInstance) {
              chartInstance.destroy();
            }
          };
        }}
      />
    </div>
  );
}

触发场景与错误日志

当searchTerms快速变化(触发延迟过渡)且用户同时切换setShowMetrics(false)时,应用崩溃,控制台报错:

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,崩溃消失。

崩溃原因解析

这是React 19并发渲染机制与回调Ref清理函数特性的交互导致的:

  1. 并发渲染与过渡状态:useDeferredValue会触发React的并发过渡渲染,此时React会保留旧的UI快照以实现平滑过渡,新旧渲染任务会并行处理。
  2. 回调Ref的清理时机:React 19中,回调Ref的清理函数会在渲染更新(回调函数变化)或组件卸载时同步执行。当searchTerms快速变化时,组件会频繁重新渲染,每次都会生成新的回调Ref函数,旧的清理函数会被立即调用以销毁旧图表实例。
  3. 竞争条件:当用户同时切换showMetrics=false触发组件卸载时,React需要销毁旧过渡快照中的DOM节点及其清理函数。但由于并发渲染的异步特性,某个清理函数执行时,对应的chartInstance可能已经被之前的过渡更新销毁,或者因闭包捕获的是已失效的变量引用,导致chartInstance变为null——即使代码中有if(chartInstance)判断,也可能因并发执行的时序问题触发错误。

对比useRef+useEffect的方案:useEffect的清理逻辑是异步执行于DOM提交之后,且会等待过渡完成后再处理,避免了并发场景下的状态不一致问题。而内联回调Ref的清理是同步执行于渲染过程中,在快速更新+卸载的场景下更容易出现竞争条件。


修复方案

方案1:回归useRef+useEffect(推荐,已验证有效)

将图表实例的初始化和清理逻辑放在useEffect中,通过依赖项控制执行时机:

import { useState, useDeferredValue, useRef, useEffect } from 'react';
import { ExternalChartEngine } from 'awesome-charts'; 

export default function Dashboard({ searchTerms }) {
  const [showMetrics, setShowMetrics] = useState(true);
  const deferredSearch = useDeferredValue(searchTerms);
  const chartContainerRef = useRef(null);
  const chartInstanceRef = useRef(null);

  useEffect(() => {
    if (!showMetrics || !chartContainerRef.current) return;

    // 初始化图表
    chartInstanceRef.current = ExternalChartEngine.init(chartContainerRef.current, {
      query: deferredSearch
    });

    // 清理函数
    return () => {
      if (chartInstanceRef.current) {
        chartInstanceRef.current.destroy();
        chartInstanceRef.current = null;
      }
    };
  }, [deferredSearch, showMetrics]);

  if (!showMetrics) return <div>Metrics Hidden</div>;

  return (
    <div className="dashboard-container">
      <div ref={chartContainerRef} />
    </div>
  );
}

方案2:优化回调Ref避免闭包问题

如果坚持使用回调Ref,将图表实例存储在useRef中,避免闭包捕获失效的变量:

import { useState, useDeferredValue, useRef } from 'react';
import { ExternalChartEngine } from 'awesome-charts'; 

export default function Dashboard({ searchTerms }) {
  const [showMetrics, setShowMetrics] = useState(true);
  const deferredSearch = useDeferredValue(searchTerms);
  const chartInstanceRef = useRef(null);

  if (!showMetrics) return <div>Metrics Hidden</div>;

  return (
    <div className="dashboard-container">
      <div 
        ref={(element) => {
          // 卸载或ref移除时清理
          if (!element) {
            if (chartInstanceRef.current) {
              chartInstanceRef.current.destroy();
              chartInstanceRef.current = null;
            }
            return;
          }
          
          // 已有实例先销毁
          if (chartInstanceRef.current) {
            chartInstanceRef.current.destroy();
          }
          // 初始化新实例
          chartInstanceRef.current = ExternalChartEngine.init(element, {
            query: deferredSearch
          });
        }}
      />
    </div>
  );
}

内容的提问来源于stack exchange,提问作者ttboy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.02 02:17:27