使用React.js读取CSV文件并通过Chart.js展示时canvas复用报错如何解决
问题排查与解决方案
错误根因
报错本质是同一个canvas画布被重复初始化了Chart实例,且没有正确销毁之前的实例,结合你提供的代码,具体错误写法如下:
- 没有把数据请求、Chart实例初始化逻辑放在副作用钩子
useEffect里,组件每一次重渲染都会触发d3.csv请求,请求成功后就会尝试初始化新的Chart实例 - 用来存Chart实例的
myChart是组件内部的普通变量,组件重渲染后变量会被重置,你根本拿不到上一次渲染生成的Chart实例,自然也没法正确调用destroy方法 - 直接使用
document.getElementById获取DOM元素,不符合React操作DOM的规范,容易拿到过期的DOM节点 - 你已经引入了
react-chartjs-2封装好的Chart组件,却选择手动实例化Chart,相当于放弃了封装层自带的实例生命周期管理能力
修复方案
方案1:改用react-chartjs-2封装组件(推荐)
直接用封装好的Line组件,不需要手动管理实例的创建和销毁,封装层已经帮你处理了生命周期:
import React, { useState, useEffect } from 'react' import { Line } from 'react-chartjs-2' import * as d3 from 'd3' import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, } from 'chart.js' // 注册Chart.js需要的模块 ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend ) const BarChart1 = () => { // 存储图表数据 const [chartData, setChartData] = useState(null) useEffect(() => { const filename = 'drug-abusers-by-gender.csv'; d3.csv(filename).then(loadedData => { const labels = [] const data = [] // 注意:d3.csv返回的数组第一个元素已经是解析后的数据,不需要从i=1开始遍历,除非你的csv第一行是重复表头 for (let i = 0; i < loadedData.length; i++) { labels.push(loadedData[i].year) data.push(Number(loadedData[i].no_of_drug_abusers)) } setChartData({ labels, datasets: [{ data, fill: false, pointRadius: 0, pointHoverRadius: 0, borderColor: 'rgb(100,100,100)' }] }) }) }, []) // 空依赖数组,只在组件首次挂载时执行一次请求 const options = { responsive: true, plugins: { legend: { display: false } } } // 数据加载完成前返回空,避免渲染空canvas if (!chartData) return null return <Line data={chartData} options={options} /> } export default BarChart1
方案2:手动管理Chart实例(保留手动初始化写法)
如果你一定要手动操作实例,需要用useRef存实例和canvas元素,同时在组件卸载时销毁实例:
import React, { useEffect, useRef } from 'react' import { Chart } from 'chart.js' import * as d3 from 'd3' const BarChart1 = () => { // 存canvas元素的ref const canvasRef = useRef(null) // 存Chart实例的ref,重渲染不会重置 const chartRef = useRef(null) useEffect(() => { const filename = 'drug-abusers-by-gender.csv'; d3.csv(filename).then(loadedData => { const labels = [] const data = [] for (let i = 0; i < loadedData.length; i++) { labels.push(loadedData[i].year) data.push(Number(loadedData[i].no_of_drug_abusers)) } // 初始化前先销毁已存在的实例 if (chartRef.current) { chartRef.current.destroy() } // 初始化新实例 chartRef.current = new Chart(canvasRef.current, { type: 'line', data: { labels, datasets: [{ data, fill: false, pointRadius: 0, pointHoverRadius: 0, borderColor: 'rgb(100,100,100)' }] } }) }) // 组件卸载时销毁实例,避免内存泄漏和重复实例报错 return () => { if (chartRef.current) { chartRef.current.destroy() } } }, []) return <canvas ref={canvasRef}></canvas> } export default BarChart1
额外注意点
- 你的代码中原来的
d3.csv用了回调写法,建议改成Promise写法更易维护,也避免回调地狱 - 从csv读取的
no_of_drug_abusers是字符串类型,需要转成数字再传入Chart,否则图表可能渲染异常 - 如果你的csv文件第一行是表头,d3.csv解析后会自动跳过表头把第一行数据作为数组第一个元素,不需要从i=1开始遍历,否则会丢失第一条数据
内容的提问来源于stack exchange,提问作者Kok Chong
相关产品推荐
相关产品推荐

