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.