ECharts 6 in React 19

Published on

This is essentially an update on my last post.

The previous wrapper would work just as well with React v19 and eCharts v6, however, React 19 does gain a new way to support refs that simplifies the wrapper somewhat.

Additionally, the window resize event can be problematic, triggering when not necessary, not triggering when expected, such as when a collapsible side bar opens and closes, changing the size of the section where the chart is, but not triggering a resize event. ResizeObserver support at this point seems to be good enough, so the switch is made to use that as well.

I have noticed an edge case bug with resize observer. I haven’t been able to determine if it is a browser bug or what exactly, but if using CSS grid with flexible units eg. grid-template-columns: repeat(2, 1fr); ResizeObserver does not reliably trigger on shrinking in Chromium based browsers.

Anyway, here is a React 19 version:

import type {
	ECharts,
	EChartsInitOpts,
	EChartsOption,
	EChartsType,
	SetOptionOpts,
} from "echarts";
import { init } from "echarts";
import type { CSSProperties, Ref } from "react";
import {
	useCallback,
	useEffect,
	useImperativeHandle,
	useMemo,
	useRef,
} from "react";

import * as echarts from "echarts";
import chartTheme from "public/JSON/chartTheme.json" with { type: "json" };

// Register the theme ONCE at the module level
echarts.registerTheme("chartTheme", chartTheme);

export interface ReactEChartsProps {
	initOptions?: EChartsInitOpts;
	option: EChartsOption;
	style?: CSSProperties; // Custom CSS styles for the chart container
	settings?: SetOptionOpts; // Additional ECharts settings
	showLoading?: boolean; // Flag to show or hide the loading animation
	theme?: string | Record<string, any>; // Theme for the ECharts instance
	onEvents?: Record<
		string,
		(this: EChartsType, event: unknown) => boolean | void
	>; // Event handlers for ECharts events
	ref?: Ref<EChartsInstance>; // React 19 gains native ref support
}

export interface EChartsInstance {
	getEchartsInstance: () => ECharts | null;
}

/**
 * A wrapper to make Echarts (which is not React based), able to be used like a React component to improve developer ergonomics
 */
function ReactECharts({
	initOptions = {},
	option,
	style = {},
	settings,
	showLoading = false,
	theme = "chartTheme",
	onEvents = {},
	ref,
}: ReactEChartsProps) {
	// for the chart container div
	const chartRef = useRef<HTMLDivElement>(null);
	// for the ECharts instance
	const chartInstance = useRef<ECharts | null>(null);
	// for resizeObserver
	const roRef = useRef<ResizeObserver | null>(null);

	useImperativeHandle(
		ref,
		(): EChartsInstance => ({
			getEchartsInstance: () => chartInstance.current,
		}),
		[]
	);

	const resizeChart = useCallback(() => {
		requestAnimationFrame(() => {
			chartInstance.current?.resize();
		});
	}, []);

	// for better performance
	const eventKeys = useMemo(() => Object.keys(onEvents), [onEvents]);

	const initializeChart = useCallback(() => {
		if (chartRef.current !== null) {
			if (!chartInstance.current) {
				chartInstance.current = init(chartRef.current, theme, {
					width: "auto",
					height: "auto",
					...initOptions,
				});
			}

			chartInstance.current.setOption(option, settings);

			// optimized binding with memoized events
			eventKeys.forEach((eventName) => {
				const handler = onEvents[eventName];
				if (handler) {
					chartInstance.current?.on(eventName, (...args) => {
						handler.apply(chartInstance.current, args);
					});
				}
			});

			if (showLoading) {
				chartInstance.current.showLoading();
			} else {
				chartInstance.current.hideLoading();
			}

			roRef.current = new ResizeObserver(resizeChart);
			roRef.current.observe(chartRef.current);

			// Return cleanup function
			return () => {
				eventKeys.forEach((eventName) => {
					const handler = onEvents[eventName];
					if (handler) {
						chartInstance.current?.off(eventName, handler);
					}
				});

				chartInstance.current?.dispose();
				chartInstance.current = null;
				roRef.current?.disconnect();
				roRef.current = null;
			};
		}
	}, [
		initOptions,
		option,
		settings,
		eventKeys,
		onEvents,
		showLoading,
		resizeChart,
	]);

	useEffect(() => {
		const cleanup = initializeChart();
		return cleanup;
		// Future: Consider useEffectEvent when it becomes stable for event handlers
	}, [initializeChart]);

	return (
		<div ref={chartRef} role="region" className="eChartWrapper" style={style} />
	);
}

ReactECharts.displayName = "ReactECharts";

export default ReactECharts;

Echarts v6 has a lot of cool new stuff, but nothing much needs to change with this wrapper to accommodate that. One of the new features is an option to dynamically change theme; theoretically you could add a useEffect to handle that with something like this in it.

// Switch themes dynamically
chartInstance.current.dispose();
initializeChart();

I haven’t had an excuse or opportunity to check how that might work yet.