React useEffect初始化Chart.js报错:Cannot read properties of null (reading 'getContext')
React中useRef+Chart.js异步数据初始化的时序问题解决
问题描述
在React组件中使用useRef定位canvas元素,初始化Chart.js图表实例:
- 用硬编码静态数据初始化时完全正常;
- 组件挂载后异步从API获取数据,待数据返回后初始化图表,抛出致命错误导致应用崩溃。
错误控制台输出:
Uncaught TypeError: Cannot read properties of null (reading 'getContext') at AnalyticsDashboard.js:31:35 at commitHookEffectListMount (react-dom.development.js:23150:26) at commitPassiveMountOnFiber (react-dom.development.js:24408:13)
已尝试方案:
- 在调用
getContext前添加if (!canvasRef.current) return;:错误消失,但图表永远不会渲染; - 移除
if (loading) return...提前返回逻辑:图表正常渲染,但页面会先显示空的canvas,体验异常。
错误原因分析
核心问题是状态更新的时序不匹配:
- API请求完成后,会先执行
setAnalyticsData(data)更新数据状态,此时loading仍为true; - 组件重新渲染时,因为
loading为true,会直接返回加载提示的<div>,canvas元素并未被渲染到DOM中,canvasRef.current保持为null; - 监听
analyticsData的useEffect会因为数据状态更新触发执行,此时尝试调用canvasRef.current.getContext()自然会抛出null读取属性的错误。
而第一种尝试方案中,虽然避免了错误,但当后续loading变为false、canvas渲染完成时,analyticsData没有再次变化,监听它的useEffect不会重新执行,所以图表永远不会初始化。
修复方案
方案1:同步监听数据与加载状态
调整初始化图表的useEffect依赖,同时监听analyticsData和loading,确保数据已获取且canvas已渲染后再初始化图表:
import { useEffect, useRef, useState } from 'react'; import Chart from 'chart.js/auto'; export default function AnalyticsDashboard() { const canvasRef = useRef(null); const [analyticsData, setAnalyticsData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchMetrics() { try { const response = await fetch('/api/v1/analytics'); const data = await response.json(); setAnalyticsData(data); } catch (err) { console.error("Failed to fetch", err); } finally { setLoading(false); } } fetchMetrics(); }, []); useEffect(() => { // 必须同时满足:数据存在、加载完成、canvas已挂载 if (!analyticsData || loading || !canvasRef.current) return; const ctx = canvasRef.current.getContext('2d'); const chartInstance = new Chart(ctx, { type: 'bar', data: { labels: analyticsData.labels, datasets: [{ data: analyticsData.values }] } }); return () => chartInstance.destroy(); }, [analyticsData, loading]); // 添加loading作为依赖 if (loading) return <div>Loading metrics...</div>; return ( <div className="chart-container"> <h2>Monthly Traffic</h2> <canvas ref={canvasRef} id="trafficChart" width="400" height="200"></canvas> </div> ); }
方案2:调整渲染逻辑,始终保留canvas(可选)
如果希望加载状态下也保留canvas元素(通过CSS隐藏),可以避免ref为空的情况:
import { useEffect, useRef, useState } from 'react'; import Chart from 'chart.js/auto'; export default function AnalyticsDashboard() { const canvasRef = useRef(null); const [analyticsData, setAnalyticsData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchMetrics() { try { const response = await fetch('/api/v1/analytics'); const data = await response.json(); setAnalyticsData(data); } catch (err) { console.error("Failed to fetch", err); } finally { setLoading(false); } } fetchMetrics(); }, []); useEffect(() => { if (!analyticsData || !canvasRef.current) return; const ctx = canvasRef.current.getContext('2d'); const chartInstance = new Chart(ctx, { type: 'bar', data: { labels: analyticsData.labels, datasets: [{ data: analyticsData.values }] } }); return () => chartInstance.destroy(); }, [analyticsData]); return ( <div className="chart-container"> <h2>Monthly Traffic</h2> {/* 用loading状态控制canvas的显示 */} <canvas ref={canvasRef} id="trafficChart" width="400" height="200" style={{ display: loading ? 'none' : 'block' }} ></canvas> {loading && <div>Loading metrics...</div>} </div> ); }
关键说明
- React的ref只有在对应元素被渲染到DOM后才会赋值,所以必须确保元素已挂载再读取ref;
useEffect的依赖数组必须包含所有影响逻辑执行的状态,否则会出现时序问题;- 销毁Chart实例的清理函数很重要,避免组件卸载或状态更新时出现内存泄漏。
内容的提问来源于stack exchange,提问作者ttboy
相关产品推荐
相关产品推荐

