如何强制重渲染react-chartjs-2的Chart组件并更新自定义插件?
问题:react-chartjs-2自定义Tooltip插件主题切换后需刷新才生效
我为react-chartjs-2的折线图实现了一个自定义插件,让所有数据点的Tooltip支持深色(#151429)和浅色(#FFF)两种背景色,但切换主题后,Tooltip样式必须刷新页面才会更新,无法实时生效。
原代码
import React, { useEffect, useState } from "react"; import { Chart as ChartJS} from "chart.js"; import { Line } from "react-chartjs-2"; import faker from "faker"; const labels = ["January", "February", "March", "April", "May", "June", "July"]; export function App() { const [theme, setTheme] = useState(() => { const initialTheme = localStorage.getItem("theme"); return initialTheme ? initialTheme : "light"; }); function getThemeFromLocalStorage() { const savedTheme = localStorage.getItem("theme"); if (savedTheme) { setTheme(savedTheme); } } function toggleTheme() { setTheme((prevTheme) => { const newTheme = prevTheme === "light" ? "dark" : "light"; localStorage.setItem("theme", newTheme); return newTheme; }); } useEffect(() => { getThemeFromLocalStorage(); }, [theme]); const allTooltips = theme?.startsWith("d") ? "#151429" : "#FFF"; const data = { labels, datasets: [ { fill: true, label: "Dataset 2", data: labels.map(() => faker.datatype.number({ min: 0, max: 1000 })), borderColor: "rgb(53, 162, 235)", backgroundColor: "rgba(53, 162, 235, 0.5)", }, ], }; const alwaysShowTooltip = { id: "alwaysShowTooltip", afterDatasetsDraw(chart) { const { ctx } = chart; ctx.save(); chart.data.datasets.forEach((dataset, i: number) => { chart.getDatasetMeta(i).data.forEach((datapoint, index: number) => { const { x, y } = datapoint.tooltipPosition(); const itemText = chart.data.datasets[i].data[index]; const text = itemText !== null ? itemText + "%" : ""; ctx.beginPath(); ctx.fillStyle = itemText !== null ? allTooltips : "transparent"; const textWidth = ctx.measureText(text).width; ctx.stroke(); ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 5; ctx.shadowBlur = 5; ctx.shadowColor = "rgba(0, 0, 0, 0.15)"; ctx.roundRect( x - (textWidth + 10) / 2, y - 25, textWidth + 10, 16, 4, ); ctx.fill(); ctx.shadowColor = "transparent"; //text ctx.font = "Regular 10px Poppins"; ctx.fillStyle = "gray"; ctx.fillText(text, x - textWidth / 2, y - 14); }); }); }, }; return ( <> <Line options={options} data={data} plugins={[alwaysShowTooltip]} /> <button onClick={toggleTheme}>Toggle Theme: {theme}</button> </> ); }
问题根源
- 插件的
afterDatasetsDraw方法依赖组件外部的allTooltips变量,由于闭包特性,它只会捕获组件首次渲染时的主题值,后续主题切换后不会更新。 - 主题切换时没有触发图表的重绘逻辑,导致插件不会重新执行绘制。
解决方法及修改后的代码
import React, { useEffect, useState, useRef } from "react"; import { Chart as ChartJS } from "chart.js"; import { Line } from "react-chartjs-2"; import faker from "faker"; const labels = ["January", "February", "March", "April", "May", "June", "July"]; export function App() { const [theme, setTheme] = useState(() => { const initialTheme = localStorage.getItem("theme"); return initialTheme ? initialTheme : "light"; }); const chartRef = useRef<any>(null); function toggleTheme() { setTheme((prevTheme) => { const newTheme = prevTheme === "light" ? "dark" : "light"; localStorage.setItem("theme", newTheme); // 主题切换后手动触发图表更新 chartRef.current?.update(); return newTheme; }); } // 初始化时同步localStorage中的主题 useEffect(() => { const savedTheme = localStorage.getItem("theme"); if (savedTheme) { setTheme(savedTheme); } }, []); const alwaysShowTooltip = { id: "alwaysShowTooltip", afterDatasetsDraw(chart) { const { ctx } = chart; // 动态获取当前主题,避免闭包导致的旧值引用 const currentTheme = localStorage.getItem("theme") || "light"; const tooltipBg = currentTheme.startsWith("d") ? "#151429" : "#FFF"; // 深色模式下调整文本颜色,提升可读性 const textColor = currentTheme.startsWith("d") ? "#ccc" : "gray"; ctx.save(); chart.data.datasets.forEach((dataset, i: number) => { chart.getDatasetMeta(i).data.forEach((datapoint, index: number) => { const { x, y } = datapoint.tooltipPosition(); const itemText = chart.data.datasets[i].data[index]; const text = itemText !== null ? itemText + "%" : ""; ctx.beginPath(); ctx.fillStyle = itemText !== null ? tooltipBg : "transparent"; const textWidth = ctx.measureText(text).width; ctx.stroke(); ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 5; ctx.shadowBlur = 5; ctx.shadowColor = "rgba(0, 0, 0, 0.15)"; ctx.roundRect( x - (textWidth + 10) / 2, y - 25, textWidth + 10, 16, 4 ); ctx.fill(); ctx.shadowColor = "transparent"; // 文本样式 ctx.font = "Regular 10px Poppins"; ctx.fillStyle = textColor; ctx.fillText(text, x - textWidth / 2, y - 14); }); }); ctx.restore(); }, }; const data = { labels, datasets: [ { fill: true, label: "Dataset 2", data: labels.map(() => faker.datatype.number({ min: 0, max: 1000 })), borderColor: "rgb(53, 162, 235)", backgroundColor: "rgba(53, 162, 235, 0.5)", }, ], }; return ( <> <Line ref={chartRef} data={data} plugins={[alwaysShowTooltip]} /> <button onClick={toggleTheme}>切换主题:{theme}</button> </> ); }
关键修改说明
- 使用
useRef获取图表实例,主题切换时调用update()方法强制图表重绘,触发插件重新执行绘制逻辑。 - 在插件内部直接从
localStorage读取当前主题,绕过闭包限制,确保每次绘制都使用最新的主题值。 - 优化深色模式下的文本颜色,解决原代码中灰色文本在深色背景下可读性差的问题。
- 移除原代码中可能导致无限循环的
useEffect依赖(原useEffect依赖theme且内部调用setTheme,会触发重复渲染)。
内容的提问来源于stack exchange,提问作者Yuriy Paraska
相关产品推荐
相关产品推荐

