reactjsruby-on-railsviteapexchartsflowbite

ViteJS version 5 not working with Apexcharts


I'm currently working on a project using Ruby on Rails and React. I'm using Vite vite_rails and vite version 5.3.1. I'm trying to integrate some dynamic charts using Apexcharts examples from Flowbite. enter image description here

As shown in the image below when I run vite dev and start the server I see the chart. But when I run vite build the donut chart doesn't seem to compile. enter image description here

Im unable to identify why. My hunch is that there must be an issue with the configuration of the Vite config.

vite.config.mjs

import { defineConfig } from 'vite';
import RubyPlugin from 'vite-plugin-ruby';
import svgr from 'vite-plugin-svgr';

export default defineConfig({
  plugins: [
    RubyPlugin(),
    svgr({
      svgrOptions: {
        exportType: 'named',
        ref: true,
        svgo: false,
        titleProp: true,
      },
      include: '**/*.svg',
    }),
    muteWarningsPlugin(warningsToIgnore),
  ],
  build: {
    minify: false,
    terserOptions: {
      compress: {
        keep_infinity: true,
        drop_console: false, // Keep console.log() statements
      },
    },
    sourcemap: true, // Ensure sourcemaps are enabled
  },
});

Based on this example, for the Donut Chart from Flowbite is that Im setting it.

DonutChart.jsx

import React, { useEffect, useRef, useState } from 'react';
import ApexCharts from 'apexcharts';
import LoadingSpinner from './LoadingSpinner';

const DonutChart = () => {
  const chartRef = useRef(null);
  const [series, setSeries] = useState([0, 0, 0]);
  const [loading, setLoading] = useState(true);

  const getChartOptions = (stats) => {
    return {
      series: series,
      colors: ["#022963", "#e74694", "#FDBA8C"],
      chart: {
        height: 320,
        width: "100%",
        type: "donut",
      },
      stroke: {
        colors: ["transparent"],
        lineCap: "",
      },
      plotOptions: {
        pie: {
          donut: {
            labels: {
              show: true,
              name: {
                show: true,
                fontFamily: "Inter, sans-serif",
                offsetY: 20,
              },
              total: {
                showAlways: true,
                show: true,
                label: "Total Assets",
                fontFamily: "Inter, sans-serif",
                formatter: function () {
                  return stats.total_assets;
                },
              },
              value: {
                show: true,
                fontFamily: "Inter, sans-serif",
                fontWeight: 'bold',
                offsetY: -20,
                formatter: function (value) {
                  return value + "%"
                },
              },
            },
            size: "80%",
          },
        },
      },
      grid: {
        padding: {
          top: -2,
        },
      },
      labels: ["Operational", "Not Operational", "Under repair"],
      dataLabels: {
        enabled: false,
      },
      legend: {
        position: "bottom",
        fontFamily: "Inter, sans-serif",
      },
      yaxis: {
        labels: {
          formatter: function (value) {
            return value + "%"
          },
        },
      },
      xaxis: {
        labels: {
          formatter: function (value) {
            return value + "%"
          },
        },
        axisTicks: {
          show: false,
        },
        axisBorder: {
          show: false,
        },
      },
    }
  }

  useEffect(() => {
    let chartInstance = null;

    const fetchData = async () => {
      try {
        const response = await fetch('/api/v1/equipment_stats');
        const stats = await response.json();

        const newSeries = [
          stats.operational || 0,
          stats.not_operational || 0,
          stats.under_repair || 0,
        ];

        setSeries(newSeries);
        setLoading(false);

        if (!chartInstance && chartRef.current) {
          const chartOptions = getChartOptions(stats);
          chartOptions.series = newSeries.filter(n => n !== null && n !== undefined); // Ensure no null/undefined values
          chartInstance = new ApexCharts(chartRef.current, chartOptions);
          chartInstance.render();
        } else if (chartInstance) {
          chartInstance.updateSeries(newSeries);
        }
      } catch (error) {
        console.error("Error fetching data:", error);
        setLoading(false);
        // Optionally, handle the UI here for error state
      }
    };

    fetchData();

    return () => {
      if (chartInstance) {
        chartInstance.destroy();
      }
    };
  }, []);


  if (loading) {
    return <LoadingSpinner />;
  }

  return (
    <div className="py-4">
      <h2 className="text-center font-semibold my-4">Equipment Health</h2>
      <div ref={chartRef}></div>
    </div>
  );
}

export default DonutChart;

here is other related information

package.json

{
  "name": "aviato",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "@emotion/react": "^11.11.3",
    "@emotion/styled": "^11.11.0",
    "@headlessui/react": "^1.7.15",
    "@heroicons/react": "^2.0.18",
    "@mui/icons-material": "^5.15.10",
    "@mui/material": "^5.15.10",
    "@rails/activestorage": "^7.0.6",
    "@stripe/react-stripe-js": "^2.4.0",
    "@stripe/stripe-js": "^2.4.0",
    "@tailwindcss/forms": "^0.5.3",
    "activestorage": "^5.2.8-1",
    "apexcharts": "^3.44.0",
    "clsx": "^2.1.0",
    "crypto-js": "^4.2.0",
    "react": "^18.2.0",
    "react-currency-input-field": "^3.6.11",
    "react-dom": "^18.2.0",
    "react-hook-form": "^7.48.2",
    "react-router-dom": "^6.14.0",
    "tailwindcss": "^3.3.2"
  },
  "devDependencies": {
    "autoprefixer": "^10.4.14",
    "postcss": "^8.4.24",
    "tailwindcss": "^3.3.2",
    "vite": "^5.3.1",
    "vite-plugin-ruby": "^5.0.0",
    "vite-plugin-svgr": "^4.2.0"
  },
  "volta": {
    "node": "20.10.0"
  }
}

This is a bug I been fighting with for a couple of weeks. I been raised the issue with the Vite team. https://github.com/vitejs/vite/issues/17516

Gemfile.lock

    vite_rails (3.0.17)
      railties (>= 5.1, < 8)
      vite_ruby (~> 3.0, >= 3.2.2)

Solution

  • I'm not sure how your example works when you run vite dev. I tried it with a simpler example so I can run it, it didn't work in either case. You have a bit of a race condition between when useEffect is called and when chartRef.current is set.

    optional dependencies: The list of all reactive values referenced inside of the setup code. Reactive values include props, state, and all the variables and functions declared directly inside your component body.
    ...
    If you omit this argument, your Effect will re-run after every re-render of the component.

    https://react.dev/reference/react/useEffect#parameters

    If you log chartRef.current it is null on the first render. You must be triggering a rerender somewhere for this set up to work.

    Declaring loading as a dependency was enough for me:

    useEffect(() => {
      let chartInstance = null;
      setLoading(false);
    
      console.log(chartRef); // initial:  {current: null}
                             // rerender: {current: div}
    
      if (!chartInstance && chartRef.current) {
        chartInstance = new ApexCharts(chartRef.current, getChartOptions({}));
        chartInstance.render();
      }
    
      return () => {
        if (chartInstance) {
          chartInstance.destroy();
        }
      };
    }, [loading]);
    //  ^^^^^^^