Search code examples
javascriptreactjsreact-hookschart.jsreact-chartjs-2

How can I force a re-render react component Chart(react-chart.js) and update the custom plugin?


I made tooltips for all points using the plugin with two background colors dark and light. But the plugin works only after refreshing page. I made a code sandbox to show a problem. When you'll open sandbox, you'll see a button toggle. Click on dark theme and refresh page.

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>
    </>
  );
}

CodeSandbox

I tried some ideas and looked for decisions but haven't found any.


Solution

  • A very trivial solution would be to use a React key on the Line component that updates when the theme state updates.

    Example using the theme state value as the key:

    <Line
      key={theme} // <--
      options={options}
      data={data}
      plugins={[alwaysShowTooltip]}
    />
    

    The idea here is that the Line component will remount and use the current computed data and options for rendering the chart.