# react-use-echarts
> [δΈζ](./README-zh_CN.md) | [English](./README.md)
[](https://www.npmjs.com/package/react-use-echarts)
[](https://www.npmjs.com/package/react-use-echarts)
[](https://github.com/chensid/react-use-echarts/actions/workflows/ci.yml)
[](https://codecov.io/gh/chensid/react-use-echarts)
[](https://github.com/chensid/react-use-echarts/issues)
[](https://github.com/chensid/react-use-echarts/pulls)
[](https://github.com/chensid/react-use-echarts/blob/main/LICENSE.txt)
[](https://bundlephobia.com/package/react-use-echarts)
[](https://www.npmjs.com/package/react-use-echarts)
React hooks & component for Apache ECharts β TypeScript, auto-resize, themes, lazy init.
**[π Live demo & interactive playground β](https://chensid.github.io/react-use-echarts/)**
[](https://chensid.github.io/react-use-echarts/)
## Features
- **Hook + Component** β use `useEcharts` hook or the declarative `` component
- **TypeScript first** β complete type definitions with IDE autocomplete
- **Zero dependencies** β no runtime deps beyond peer deps
- **Auto-resize** β handles container resizing via ResizeObserver
- **Themes** β built-in light, dark, and macarons themes, plus any custom theme
- **Chart linkage** β connect multiple charts for synchronized interactions
- **Lazy initialization** β defer chart init until element enters viewport
- **StrictMode safe** β instance cache with reference counting handles double mount/unmount
## Why react-use-echarts?
A modern, hook-first wrapper for teams on **React 19 + ECharts 6**. ECharts stays the single source of truth β you pass `EChartsOption` straight through, with no abstraction layer to re-learn.
| | react-use-echarts | echarts-for-react |
| ------------- | ------------------------------------------------- | ------------------------ |
| API | `useEcharts` hook **and** `` component | Component only |
| Built for | React 19 β callback ref, StrictMode-safe | React 16β18 era |
| Auto-resize | `ResizeObserver` + RAF, on by default | β |
| Lazy init | Built-in `lazyInit` (IntersectionObserver) | Manual |
| Chart linkage | Built-in `group` prop | Manual `echarts.connect` |
| Error routing | `onError` for chart operations and imperative API | Manual try/catch |
| Format & deps | ESM-only, tree-shakeable, zero runtime deps | CJS + ESM, zero deps |
Already using `echarts-for-react`? Most props map 1:1 β see [Migrating from echarts-for-react](#migrating-from-echarts-for-react).
## Requirements
- React 19.2+ (`react` + `react-dom`) β `useEffectEvent` is used internally and reached stable in 19.2
- ECharts 6.x
- Node.js 22+ (required only for tooling/SSR frameworks β the published bundle is browser ESM)
> **CSR only.** ECharts needs a live DOM; SSR is not supported.
>
> **ESM-only since 1.3.0.** The package publishes a single ESM build (`dist/index.js`). Every modern bundler (Vite, Next.js, webpack 5+, Rspack, Parcel, Turbopack) and Node 22+ (`require(ESM)`) consume it natively. If you still depend on CJS-only tooling, pin to `1.2.x`.
## Installation
```bash
npm install react-use-echarts echarts
# or
yarn add react-use-echarts echarts
# or
pnpm add react-use-echarts echarts
```
## Register ECharts modules
Since v2.1 `react-use-echarts` is fully modular β it does not auto-register any ECharts chart, component, renderer or feature. Call one of the registrars below **once at your application entry**, before the first chart renders:
```ts
// Simplest β registers everything ECharts ships with (~290KB gzip).
import { registerEchartsFull } from "react-use-echarts/preset-full";
registerEchartsFull();
```
Or, for tree-shake-friendly production builds, register only what you actually render β see [Tree-shaking](#tree-shaking) for the recipe.
> **Why?** Production minifiers (Rolldown/Oxc, Rollup) drop ECharts' top-level `use([...])` side-effect registrations as pure because the upstream package's `sideEffects` field is non-conforming. Moving registration to the consumer side mirrors what `vue-echarts`, `nuxt-echarts` and `react-chartjs-2` do, and keeps `react-use-echarts` reliable across every modern bundler.
## Quick Start
### 1. Register ECharts Once
For the fastest start, register the full ECharts surface at your app entry:
```ts
// main.tsx / index.tsx
import { registerEchartsFull } from "react-use-echarts/preset-full";
registerEchartsFull();
```
For production bundles that only render a few chart types, replace this with selective `echarts.use([...])` registration later; the chart API stays the same.
### 2. Render a Chart
The simplest component path β no ref needed:
```tsx
import { EChart } from "react-use-echarts";
function MyChart() {
return (
);
}
```
The chart container must have an explicit size. The example above sets it on ``; if you keep the default `{ width: "100%", height: "100%" }`, make sure the parent has an explicit height.
Pass `ref` to access the imperative API β see [Returns](#returns) for the full list (`setOption`, `dispatchAction`, `clear`, `resize`, `appendData`, `getDataURL`, `convertToPixel`, β¦).
### 3. Use the Hook Directly
For full control, use the hook directly. It returns a callback `ref` to attach to your container plus a reactive `instance` field and the full imperative API:
```tsx
import { useEcharts } from "react-use-echarts";
function MyChart() {
const { ref, instance, setOption, resize } = useEcharts({
option: {
xAxis: { type: "category", data: ["Mon", "Tue", "Wed", "Thu", "Fri"] },
yAxis: { type: "value" },
series: [{ data: [150, 230, 224, 218, 135], type: "line" }],
},
});
return
;
}
```
`instance` is `undefined` before init and after dispose; subscribe via `useEffect([instance])` to run side effects against the live ECharts instance.
The chart container must have an explicit size, for example `style={{ width: "100%", height: "400px" }}`.
## Recipes
### Themes
Built-in themes require one-time registration at app startup:
```tsx
import { registerBuiltinThemes } from "react-use-echarts/themes/registry";
registerBuiltinThemes();
// Built-in theme
useEcharts({ option, theme: "dark" });
// Any string registered via echarts.registerTheme
useEcharts({ option, theme: "vintage" });
// Custom theme object (use useMemo to keep reference stable)
const customTheme = useMemo(() => ({ color: ["#fc8452", "#9a60b4", "#ea7ccc"] }), []);
useEcharts({ option, theme: customTheme });
```
> Note: theme names registered directly via `echarts.registerTheme()` (like `"vintage"` above) work, but trigger a one-time dev-only warning because the library cannot see that registration. Prefer `registerCustomTheme(name, config)` from `react-use-echarts` β it registers the theme by name through the library, which silences the warning. If you must register via `echarts.registerTheme()` (e.g. a third-party package does it for you), the warning is safe to ignore as long as registration happens before the chart mounts; it never appears in production builds.
### Event Handling
Supports shorthand (function) and full config (object with query/context). Known echarts events have their `params` type auto-inferred from `EChartsEventPayloadMap` β no manual cast needed.
```tsx
useEcharts({
option,
onEvents: {
// `params` is auto-typed as `ECElementEvent`
click: (params) => console.log("clicked", params.data),
mouseover: {
handler: (params) => console.log("hovered", params.value),
query: "series",
},
// `params` is auto-typed as `SelectChangedPayload`
selectchanged: (params) => console.log("selection changed", params),
},
});
```
Custom event names (e.g. registered via `echarts.registerAction()`) fall through to the open index signature with a loose `params` type. To get a typed payload for your own events, augment `EChartsEventPayloadMap`:
```ts
declare module "react-use-echarts" {
interface EChartsEventPayloadMap {
"my-custom-action": { foo: number; bar: string };
}
}
```
### Loading State
```tsx
const [loading, setLoading] = useState(true);
useEcharts({
option,
showLoading: loading,
loadingOption: { text: "Loading..." },
});
```
### Chart Linkage
Assign the same `group` ID β tooltips, highlights, and other interactions will sync:
```tsx
useEcharts({ option: option1, group: "dashboard" });
useEcharts({ option: option2, group: "dashboard" });
```
### Lazy Initialization
Defer chart init until the element scrolls into view:
```tsx
useEcharts({ option, lazyInit: true });
// Custom IntersectionObserver options
useEcharts({
option,
lazyInit: { rootMargin: "200px", threshold: 0.5 },
});
```
> Note: lazy init is a one-shot latch β "lazy" means "defer until first visible", not "track visibility". Once the element has intersected, the chart stays initialized for the hook's lifetime: replacing the container DOM node or toggling `lazyInit` off and back on does not re-arm observation. To start deferring again, remount the component.
### Tree-shaking
The library is fully modular β pick the registration tier that matches your build target:
**Tier 1 β All-in-one (development / prototyping).** One line, ~290KB gzip:
```ts
import { registerEchartsFull } from "react-use-echarts/preset-full";
registerEchartsFull();
```
**Tier 2 β Selective (recommended for production).** Register only what you render β bundlers tree-shake the rest of ECharts away:
```ts
import * as echarts from "echarts/core";
import { LineChart } from "echarts/charts";
import { GridComponent, TooltipComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
echarts.use([LineChart, GridComponent, TooltipComponent, CanvasRenderer]);
```
See [`examples/selective-registration/SelectiveRegistrationChart.tsx`](./examples/selective-registration/SelectiveRegistrationChart.tsx) for a runnable demo.
**Tier 3 β Webpack-only legacy.** Webpack tolerates ECharts' non-conforming `sideEffects` field, so plain `import "echarts";` still works in webpack apps but **fails silently under Rolldown/Vite/Rollup** (chart never paints, console shows `TypeError` from zrender's empty painter registry). Prefer Tier 1 or Tier 2 instead.
> ECharts maintains a single global registry β `echarts.use([...])` and `registerEchartsFull()` compose freely. You can call them in any order, anywhere in your app, but they must run **before** the first `useEcharts()` render.
### Use with Next.js (App Router)
The default package entry, `preset-full`, and `themes/registry` are marked
with `"use client"`, so importing them inside any React Server Component file
does **not** bundle ECharts into the server payload. Wrap the chart in your own
client component and import it from any Server Component:
```tsx
// app/components/MyChart.tsx
"use client";
import { EChart } from "react-use-echarts";
import { registerEchartsFull } from "react-use-echarts/preset-full";
registerEchartsFull();
export function MyChart() {
return (
);
}
```
```tsx
// app/page.tsx (Server Component) β imports the Client Component directly
import { MyChart } from "./components/MyChart";
export default function Page() {
return ;
}
```
> **Pages Router only:** if you need to load the chart inside `getServerSideProps` /
> `getStaticProps` pages and force client-only rendering, use
> `dynamic(() => import("./components/MyChart").then((m) => m.MyChart), { ssr: false })`.
> In the **App Router**, `next/dynamic` with `ssr: false` is disallowed inside
> Server Components β the `"use client"` directive already does the right thing.
## Gotchas
- **Container needs explicit size** β the chart won't render in a zero-height div; give the container `height` (and `width` if not 100%).
- **Forgetting to register ECharts modules** β `useEcharts()` initializes a chart against ECharts' shared global registry, so charts/components/renderers/features must be registered (via `registerEchartsFull()` or `echarts.use([...])`) **before** the first render. A missing registration usually shows up as `Renderer 'undefined' is not imported` or a chart that silently never paints; see [Register ECharts modules](#register-echarts-modules). In dev, if init throws `β¦ is not a constructor`, the library also prints a one-time hint pointing you here.
- **Keep `onEvents` reference stable** β a new `onEvents` object on each render triggers a full rebind. Memoize it with `useMemo` (or hoist) when handlers don't change.
- **Don't share one DOM element across multiple `useEcharts` hooks** β the instance cache reuses a single ECharts instance and emits a dev warning; updates from different hooks will overwrite each other.
- **`initOpts` and custom `theme` objects recreate the instance on reference change** β pass memoized or module-level constants unless recreation is intended.
- **StrictMode is safe** β double mount/unmount is handled by the reference-counted instance cache.
## API Reference
### `` Props
Declarative component wrapping `useEcharts`. Accepts all hook options as props plus:
| Prop | Type | Default | Description |
| ----------- | --------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `style` | `React.CSSProperties` | `{ width: '100%', height: '100%' }` | Container style (merged with defaults) |
| `className` | `string` | β | Container CSS class |
| `ref` | `Ref` | β | Exposes the imperative API as `EChartHandle` (`Omit` β the container ref is owned by `` itself) |
### `useEcharts(options)`
#### Options
| Option | Type | Default | Description |
| --------------- | ------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `option` | `EChartsOption` | (required) | ECharts configuration |
| `theme` | `string \| object` | β | Any registered theme name, or custom theme object |
| `renderer` | `'canvas' \| 'svg'` | `'canvas'` | Renderer type |
| `lazyInit` | `boolean \| IntersectionObserverInit` | `false` | Lazy initialization via IntersectionObserver |
| `group` | `string` | β | Chart linkage group ID |
| `setOptionOpts` | `SetOptionOpts` | β | Default options for `setOption` calls |
| `showLoading` | `boolean` | `false` | Show loading indicator |
| `loadingOption` | `object` | β | Loading indicator configuration |
| `onEvents` | `EChartsEvents` | β | Event handlers (`fn` or `{ handler, query?, context? }`) |
| `autoResize` | `boolean` | `true` | Auto-resize via ResizeObserver |
| `initOpts` | `EChartsInitOpts` | β | Passed to `echarts.init()` (devicePixelRatio, locale, width, etc.) |
| `onError` | `(error: unknown) => void` | β | Error handler for chart operations and imperative API calls. Without it, effect failures log via `console.error`; imperative methods rethrow |
#### Returns
> Prefer the declarative props (`option`, `theme`, `showLoading`, β¦) over imperative methods. Use these methods only when a prop does not cover the action β image export, coordinate conversion, streaming append, etc.
> All methods are no-ops or return safe defaults when the instance is not yet initialized. When the instance throws, errors are routed through `onError` if provided (and the call returns the fallback); otherwise the error is rethrown β including from readers (no `console.error` fallback for imperative methods).
**Container ref / live instance**
| Property | Type | Description |
| ---------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ref` | `RefCallback` | Callback ref to attach to the chart container. Compose with your own ref via [`mergeRefs`](#other-exports) |
| `instance` | `ECharts \| undefined` | Reactive β defined after init, `undefined` before init and after dispose. Subscribe via `useEffect([instance])` to run side effects on the live instance |
**Lifecycle / updates**
| Method | Type | Description |
| ---------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setOption` | `(option: EChartsOption, opts?: SetOptionOpts) => void` | Update chart configuration |
| `dispatchAction` | `(payload: Payload, opt?: boolean \| { silent?: boolean; flush?: boolean }) => void` | Dispatch an ECharts action (`highlight`, `downplay`, `showTip`, etc.) |
| `clear` | `() => void` | Clear current chart content |
| `resize` | `(opts?: ResizeOpts) => void` | Manually trigger chart resize. `ResizeOpts` accepts `width`/`height`/`animation`/`silent` |
| `appendData` | `(params: { seriesIndex: number; data: ArrayLike }) => void` | Append data to a series (streaming). Drift-aware: drops dedup memory so a subsequent shallow-equal-but-new-ref `option` rerender re-applies setOption |
**Read / introspect**
| Method | Type | Description |
| ------------ | ---------------------------------- | ---------------------------------------------------------------------------------------- |
| `getOption` | `() => EChartsOption \| undefined` | Get the current merged option |
| `getWidth` | `() => number \| undefined` | Container width in pixels |
| `getHeight` | `() => number \| undefined` | Container height in pixels |
| `getDom` | `() => HTMLElement \| undefined` | Underlying DOM container |
| `isDisposed` | `() => boolean` | Whether the instance is disposed (returns `true` when uninitialized β semantically gone) |
**Export**
| Method | Type | Description |
| --------------------- | ---------------------------------------------------------- | -------------------------------------------------------- |
| `getDataURL` | `(opts?) => string \| undefined` | Base64 image data URL (`png` / `jpeg` / `svg`) |
| `getConnectedDataURL` | `(opts?) => string \| undefined` | Combined image of all charts in the same group |
| `renderToSVGString` | `(opts?: { useViewBox?: boolean }) => string \| undefined` | Render chart to SVG string (works with the SVG renderer) |
| `getSvgDataURL` | `() => string \| undefined` | Get SVG data URL of the current chart |
**Coordinate conversion**
| Method | Type | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `convertToPixel` | `(finder: ChartFinder, value: ChartScaleValue \| ChartScaleValue[]) => number \| number[] \| undefined` | Logical β pixel coordinates |
| `convertFromPixel` | `(finder: ChartFinder, value: number \| number[]) => number \| number[] \| undefined` | Pixel β logical coordinates |
| `containPixel` | `(finder: ChartFinder, value: number[]) => boolean` | Whether a pixel point is inside the matched component (false when uninit) |
`ChartFinder` is `string | { seriesIndex?, seriesId?, β¦, geoIndex?, β¦ }` β a string shorthand or a model finder object. `ChartScaleValue` is `number | string | Date`.
### Other Exports
```tsx
import { useLazyInit } from "react-use-echarts"; // standalone lazy init hook -> { ref, isInView }
import { mergeRefs } from "react-use-echarts"; // compose multiple refs into one callback ref
import { isBuiltinTheme, isKnownTheme, registerCustomTheme } from "react-use-echarts"; // theme utils (no JSON)
import { registerBuiltinThemes } from "react-use-echarts/themes/registry"; // ~20KB theme JSON
import { registerEchartsFull } from "react-use-echarts/preset-full"; // one-line full-set registrar (see Register ECharts modules)
// All exported types: UseEchartsOptions, UseEchartsReturn, UseLazyInitReturn,
// EChartProps, EChartHandle, EChartsEvents, EChartsEventConfig, EChartsEventHandler,
// EChartsEventPayloadMap, EChartsInitOpts, BuiltinTheme, LoadingOption,
// ChartFinder, ChartScaleValue, Payload.
// EChartsOption, SetOptionOpts, ResizeOpts are also re-exported here for
// convenience (they originate in the "echarts" package), so you can import them
// from react-use-echarts alongside the types above instead of reaching into echarts.
```
`mergeRefs` returns a callback ref that fans the node out to every input β `RefObject`, legacy callback ref, or React 19 callback ref with cleanup β and isolates each invocation so a throwing 3rd-party ref can't strand the chart. Reach for it when you need both the hook-provided ref and your own:
```tsx
const myRef = useRef(null);
const { ref } = useEcharts({ option });
return ;
```
## Migrating from `echarts-for-react`
Most props map 1:1; a few are folded into existing options. Quick reference:
| `echarts-for-react` | `react-use-echarts` | Notes |
| ------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `option` | `option` | Same |
| `theme` | `theme` | Same; built-in themes need `registerBuiltinThemes()` first (see [Themes](#themes)) |
| `notMerge` / `lazyUpdate` | `setOptionOpts: { notMerge, lazyUpdate }` | Folded into a single object passed to `setOption` |
| `showLoading` | `showLoading` | Same |
| `loadingOption` | `loadingOption` | Same |
| `onEvents` | `onEvents` | Same shape; also accepts `{ handler, query?, context? }` for query/context binding |
| `onChartReady` | Subscribe to the reactive `instance` | `useEffect(() => { if (instance) onReady(instance); }, [instance])` β the returned `instance` is `undefined` before init and re-renders when init/dispose completes |
| `opts.renderer` | `renderer: 'canvas' \| 'svg'` | Promoted to a top-level option |
| `opts` (rest) | `initOpts` | Same shape (`devicePixelRatio`, `locale`, `width`, `height`, `useDirtyRect`, etc.) |
| `style` | `style` | `` defaults to `{ width: '100%', height: '100%' }` so the parent needs size |
| `className` | `className` | Same |
| `lazyUpdate` (top-level) | `setOptionOpts: { lazyUpdate: true }` | See `notMerge` row |
| `shouldSetOption` | Gate the `option` prop yourself | Top-level keys are deduped via `shallowEqual` automatically; for custom predicates (deep compare, throttling, app-state gating) memoize/skip the `option` prop in the parent component |
| `autoResize` (4.x) | `autoResize` | Same default (`true`); resize uses ResizeObserver + RAF |
| _none_ | `lazyInit` | New: defer init until the container scrolls into viewport |
| _none_ | `group` | New: chart linkage via shared group ID |
| _none_ | `onError` | New: route chart operation errors through a callback (`init`, `setOption`, events, loading, resize, group linkage, and imperative calls) |
Side-by-side example:
```tsx
// echarts-for-react
instanceRef.current = instance}
/>
// react-use-echarts
// chartRef.current?.instance replaces onChartReady
```
## Migrating from v2.x
v3 removes the legacy `react-use-echarts/core` subpath. If you already import from `react-use-echarts` and already register ECharts modules before the first chart render, no code changes are needed.
If you are upgrading from v2.0's everything-included default entry, or from an app that still relied on `import "echarts"` side effects, add **one registration call** at your application entry:
```ts
// app entry (e.g. main.tsx, index.tsx)
import { registerEchartsFull } from "react-use-echarts/preset-full";
registerEchartsFull();
```
That call is equivalent to v2.0's automatic ECharts registration and gives you the same ~290KB-gzip everything-included experience. For production builds that only render a few chart types, replace it with a selective `echarts.use([...])` β see [Tree-shaking](#tree-shaking).
Replace any remaining `from "react-use-echarts/core"` imports with `from "react-use-echarts"`.
## Migrating from v1
v2.0 flips the hook to return a callback ref + reactive `instance`, aligning with the modern community convention used by `floating-ui/react`, `react-aria`, `downshift`, and `react-hook-form`. `` external props are unchanged β only direct hook consumers and `` typings migrate.
| v1 | v2 | Notes |
| ------------------------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `const ref = useRef(); useEcharts(ref, options)` | `const { ref } = useEcharts(options)` | Hook owns the callback ref; attach it to your container |
| `getInstance()` method on the hook return | `instance` field on the same return | Reactive β re-renders when init/dispose completes; use `useEffect([instance])` to subscribe |
| `useLazyInit(ref, options)` returning `boolean` | `useLazyInit(options)` returning `{ ref, isInView }` | Same callback-ref pattern |
| `useRef(null)` for `` | `useRef(null)` for `` | `EChartHandle = Omit` β the container ref is intentionally not exposed on the imperative handle |
| Compose refs by hand | `mergeRefs(chartRef, myRef)` | New public utility (see [Other Exports](#other-exports)) |
| `engines.node >=20` | `engines.node >=22` | Tooling requirement only β published bundle is unaffected |
Side-by-side hook example:
```tsx
// v1
const chartRef = useRef(null);
const { setOption, getInstance } = useEcharts(chartRef, { option });
useEffect(() => {
getInstance()?.on("finished", handler);
}, []);
return ;
// v2
const { ref, instance, setOption } = useEcharts({ option });
useEffect(() => {
if (!instance) return;
instance.on("finished", handler);
return () => instance.off("finished", handler);
}, [instance]);
return ;
```
## Contributing
We welcome all contributions. Please read the [contributing guidelines](CONTRIBUTING.md) first.
## Changelog
Detailed changes for each release are documented in the [release notes](https://github.com/chensid/react-use-echarts/releases).
## License
[MIT](./LICENSE.txt) Β© [Ethan](https://github.com/chensid)