Component wrapper
@chart-on/react wraps the engine and the shell as components for apps built with a component-based UI framework, including apps that render on a server first. It follows the wrapper contract: the chart is created when the component mounts and destroyed when it unmounts, and changing a prop updates the chart in place instead of creating a new one.
| Export | From | What it is |
|---|---|---|
<Chart> | @chart-on/react | The engine alone, in a div. |
useChart() | @chart-on/react | The Chart of the nearest <Chart> or <ChartShell>. |
<ChartShell> | @chart-on/react/shell | The widget: toolbar, drawing rail, bottom bar and the chart. |
useChartShell() | @chart-on/react/shell | The ChartShell of the nearest <ChartShell>. |
Install
pnpm add @chart-on/react @chart-on/core
# Only for <ChartShell>:
pnpm add @chart-on/uiThe framework (version 18 or later), @chart-on/core and @chart-on/ui are peer dependencies: the app installs them once and the wrapper uses the app's copies. @chart-on/ui is optional, because <ChartShell> lives in its own entry, @chart-on/react/shell. An app that only imports @chart-on/react never loads it.
The engine
Give the component a height; the chart fills its container.
import { useMemo } from 'react';
import { createMockDataSource, dataSource } from '@chart-on/core';
import { Chart } from '@chart-on/react';
// Plugins are read once, at creation: create the list once.
const plugins = [dataSource()];
export function PriceChart({ symbol }: { symbol: string }) {
// Keep the same source across renders: a new one reloads the data.
const dataSource = useMemo(() => createMockDataSource(), []);
return (
<Chart
style={{ height: 480 }}
options={{ barSpacing: 8, plugins }}
dataSource={dataSource}
symbol={symbol}
interval="1h"
/>
);
}| Prop | Applied with | Notes |
|---|---|---|
options | createChart at mount, then chart.applyOptions | Compared by value, so an inline object does not apply again on every render. Functions inside it, such as formatters, are compared by identity: create them once (module scope, useMemo) or every render applies the options again. Leaving out a key does not reset it; pass the value you want. |
dataSource | chart.setDataSource | Changing it attaches the new source and loads symbol again. Keep it stable (useMemo, module scope). Removing the prop keeps the current source; pass null to detach it. The chart needs the dataSource() plugin in options.plugins (or builtins()); without it the source is not attached and development builds warn once (data-source-missing). |
symbol, interval | chart.setSymbol | Loads once both are set and a dataSource is given; loads again when either changes. A failed load emits data:error. |
on | chart.on | Event callbacks, see Events. |
id, className, style | Attributes of the container div. | |
children | Rendered inside the container. |
A few options are fixed when the chart is created, for example gl. Changing them in options later has no effect; remount the component on purpose with a new key instead.
One of them is plugins, which gives the chart opt-in features such as annotations, drawings and heatmaps: put plugins: chartPlugins in options, with const chartPlugins = builtins() at module scope. Create the list once, since the functions in it are compared by identity.
Reaching the chart
The component gives the app the Chart it created, with the whole imperative API. It exists only while the component is mounted: it is null during server rendering and before the first mount.
With a ref:
import { useRef } from 'react';
import type { Chart as ChartInstance } from '@chart-on/core';
import { Chart } from '@chart-on/react';
export function ChartWithButton() {
const chart = useRef<ChartInstance | null>(null);
return (
<>
<button onClick={() => chart.current?.fitContent()}>Fit</button>
<Chart ref={chart} style={{ height: 400 }} />
</>
);
}With useChart() in a child. The child renders once with null, then again with the chart once it exists, so effects that depend on it run at the right time:
import { useEffect } from 'react';
import type { Bar } from '@chart-on/core';
import { Chart, useChart } from '@chart-on/react';
function Candles({ bars }: { bars: Bar[] }) {
const chart = useChart();
useEffect(() => {
if (!chart) return;
const series = chart.addCandles();
series.setData(bars);
return () => chart.removeSeries(series);
}, [chart, bars]);
return null;
}
export function OwnData({ bars }: { bars: Bar[] }) {
return (
<Chart style={{ height: 400 }}>
<Candles bars={bars} />
</Chart>
);
}For live ticks, keep the series handle and call series.update(bar): data never travels through a remount.
Events
The on prop takes one callback per event, keyed by the chart's own event names. Only the events listed are subscribed, so leave out the busy ones (crosshair:move, frame:render) unless you use them. The payload is passed on unchanged.
<Chart
style={{ height: 400 }}
on={{
'crosshair:move': (e) => setHover(e?.time ?? null),
'data:error': (e) => console.warn(e.symbol, e.error),
}}
/>A new callback for the same event is used straight away without subscribing again, so inline functions are fine. Removing a key unsubscribes it, and unmounting unsubscribes everything.
The widget
<ChartShell> mounts the full shell. Import its stylesheet once in the app.
import { ChartShell } from '@chart-on/react/shell';
import '@chart-on/ui/style.css';
export function Terminal({ dark }: { dark: boolean }) {
return (
<ChartShell
style={{ height: 600 }}
theme={dark ? 'dark' : 'light'}
features={{ tooltip: true }}
shellOptions={{ navigator: true }}
shellOn={{ 'theme:change': (e) => console.log(e.resolved) }}
/>
);
}It takes the same options, dataSource, symbol, interval, on and container props as <Chart>, plus:
| Prop | Applied with | Notes |
|---|---|---|
theme | shell.setTheme | dark, light or system. |
palette | shell.setPalette | |
features | shell.applyFeatures | Compared by value. |
shellOptions | createChartShell at mount | Toolbar, rail, dialogs and the rest of the set-up. Read once; remount with a new key to change them. |
shellOn | shell.on | Shell event callbacks, like on. |
The ref receives the ChartShell; its chart is shell.chart. Inside the shell, useChart() returns that chart and useChartShell() the shell.
Server rendering
Both entries import on a server without touching a browser global, and both components render on a server as their empty container div. The chart is created only in the browser, after the page has loaded and the component has mounted. Nothing is needed to opt in.
Frameworks with server components
The built files start with the 'use client' marker, so frameworks that split server and client components treat <Chart> and <ChartShell> as client components: they render to HTML on the server and hydrate in the browser. You can use them from a server component, but props crossing that boundary must be serializable. A data source, a ref or an event callback is not, so put those in a small component of your own marked as a client component:
'use client';
import { useMemo } from 'react';
import { createMockDataSource } from '@chart-on/core';
import { Chart } from '@chart-on/react';
export function LiveChart({ symbol }: { symbol: string }) {
const dataSource = useMemo(() => createMockDataSource(), []);
return <Chart style={{ height: 480 }} dataSource={dataSource} symbol={symbol} interval="1h" />;
}A server component can then render <LiveChart symbol="DEMO" /> and pass it plain values.
Frameworks that render every page on the server
In frameworks without server components, where each page renders on the server and then hydrates, use the components like any other. The server outputs the container; the chart appears once the page hydrates. To skip server rendering for a page section altogether, use the framework's own client-only or lazy-loading helper; the wrapper does not need it.
Example apps
Two small apps in the repository show the components in use:
examples/react-bundler: the widget in a client-rendered app, with a theme switch and a crosshair readout throughon.examples/react-ssr:<Chart>rendered to HTML on the server and hydrated in the browser, served by a small Node server.
CI builds both and renders the server example in Node with node scripts/check-examples.mjs.
Development double mounting
Development modes that mount, unmount and mount again leave one chart and one WebGL context: the first chart is destroyed before the second is created. Tests check this, along with 100 mount and unmount cycles that leave no context or animation frame behind.