Packages
React
Declarative components and hooks over the same imperative API.
What the Package Is
@ortex-charts/react is a thin binding layer with no logic of its own. Every component
creates the corresponding imperative object in a layout effect, applies prop changes with
applyOptions or setData, and disposes it on unmount. The underlying handle is always
available through ref, so nothing is lost by using the components.
It is tested against React 17, 18 and 19, and declares them as peer dependencies with
>=17.
npm install @ortex-charts/react@ortex-charts/reactThe Components
| Component | Wraps | Handle through ref |
|---|---|---|
<Chart> |
createChart |
Chart |
<Pane> |
chart.addPane |
— (provides its id through context) |
<Series> |
chart.addSeries |
SeriesModel |
<FinancialChart> |
createFinancialChart |
FinancialChart |
<ChartShell> |
createChartShell |
ChartShell |
<Sparkline> |
createSparkline |
Sparkline |
The Financial Chart
The common case is one component with data, indicators and markers as props.
import { FinancialChart, type IndicatorSpec } from "@ortex-charts/react";
import type { Bar, SeriesMarker } from "@ortex-charts/react";
import { useMemo, useState } from "react";
const OPTIONS = { theme: "dark", timeZone: "America/New_York", resolution: "1D" } as const;
export function PriceChart({ bars, earnings }: { bars: Bar[]; earnings: SeriesMarker[] }) {
const [hover, setHover] = useState("");
const indicators = useMemo<IndicatorSpec[]>(
() => [
{ id: "ema", inputs: { length: 21 } },
{ id: "sma", inputs: { length: 200 } },
{ id: "rsi" },
],
[],
);
return (
<>
<FinancialChart
options={OPTIONS}
data={bars}
indicators={indicators}
markers={earnings}
style={{ height: 460 }}
onCrosshairMove={(e) => setHover(e.time ? new Date(e.time).toDateString() : "")}
/>
<p>{hover}</p>
</>
);
}
indicators is diffed by identity: new entries are added, missing ones removed, and the
rest are left alone rather than being torn down and recreated. The identity of an entry is
the JSON of { id, inputs, options }, or an explicit key when you supply one, so two
moving averages with different lengths are two distinct entries and an unchanged array
causes no work at all.
data and markers are applied whenever the array identity changes, so a new array on
every render means setData on every render. Memoize them or hold them in state.
options is compared shallowly at the top level and the changed keys go to
applyOptions. Two keys — seriesType and volume — take effect only at creation, because
they decide what the chart is made of.
The Core Chart With Children
For anything that is not a standard price chart, compose it.
import { Chart, Pane, Series } from "@ortex-charts/react";
<Chart options={{ theme: "light", timeZone: "UTC" }} style={{ height: 520 }}>
<Series type="candlestick" data={bars} options={{ title: "AAPL" }} />
<Pane heightRatio={0.2}>
<Series type="histogram" data={volume} options={{ title: "Volume", colorMode: "mainDirection" }} />
</Pane>
<Pane heightRatio={0.25}>
<Series type="line" data={shortInterest} options={{ title: "Short interest", align: "forwardFill" }} />
</Pane>
</Chart>
<Series> renders nothing into the DOM; it manages a series on the chart from context.
<Pane> provides its id to nested series, and options.pane on a series overrides the
enclosing pane if you need to place one explicitly.
Changing type on a <Series> recreates it. Changing heightRatio or minHeight on a
<Pane> applies live.
The Full Shell
import { ChartShell } from "@ortex-charts/react";
<ChartShell
symbol={symbol}
resolution={resolution}
layout={savedLayout}
theme={theme}
options={{
datafeed,
overlays,
toolbar: { download: true },
onLayoutChange: saveLayout,
onSymbolChange: (s) => router.push(`/chart/${s}`),
}}
style={{ height: 640 }}
/>
symbol, resolution and theme are controlled props: the shell follows them when they
change, so the chart and your application state stay in step in either direction.
layout is re-applied whenever its identity changes, which is what you want after loading a
user's saved layout asynchronously.
options is read at creation. Callbacks inside it are always invoked through their latest
value, so inline arrow functions are fine and do not need memoizing. Structural options —
datafeed, overlays, toolbar — need a remount to change, and the idiomatic way to force
one is a key:
<ChartShell key={pageKind} options={{ datafeed, overlays: overlaysFor(pageKind) }} />
Hooks
useChart and usePaneId
Inside a <Chart> or <FinancialChart>, useChart() returns the core Chart or null
until it exists after the first commit. usePaneId() returns the id of the enclosing
<Pane>. Both are how you write your own child components.
import { useChart, usePaneId } from "@ortex-charts/react";
import { useEffect } from "react";
function SessionShading() {
const chart = useChart();
const paneId = usePaneId();
useEffect(() => {
if (!chart) return;
const primitive = makeSessionShading();
chart.addPrimitive(primitive, paneId);
return () => chart.removePrimitive(primitive.id);
}, [chart, paneId]);
return null;
}
useChartEvents
The four event props — onCrosshairMove, onClick, onDblClick, onVisibleRangeChange —
are accepted directly by <Chart> and <FinancialChart>. The hook behind them is exported
for a chart you hold yourself:
useChartEvents(chart, { onCrosshairMove: setHover, onVisibleRangeChange: setRange });
Subscriptions are made once per chart instance and always call the latest handler, so handlers never need memoizing.
useLiveSource
A LiveSource must survive re-renders and be closed exactly once. useMemo does not
guarantee either. useLiveSource does: it keeps the value for the lifetime of its
dependencies, closes the previous one when they change, and closes the last one on unmount.
import { FinancialChart, useLiveSource } from "@ortex-charts/react";
import { websocketSource, type LiveItem } from "@ortex-charts/financial";
function LiveChart({ symbol, bars }: { symbol: string; bars: Bar[] }) {
const source = useLiveSource(
() =>
websocketSource<LiveItem>({
url: `wss://feed.example.com/${symbol}`,
parse: (raw) => JSON.parse(String(raw)) as LiveItem,
onOpen: (send) => send(JSON.stringify({ subscribe: symbol })),
}),
[symbol],
);
return <FinancialChart data={bars} live={source} options={{ resolution: "1" }} style={{ height: 420 }} />;
}
Anything with a close() method is closed, so the hook works for your own sources too.
Sparklines
import { Sparkline } from "@ortex-charts/react";
<Sparkline data={closes} width={120} height={28} options={{ kind: "area", colorByTrend: true }} />
The container is an inline-block span with zero line height, so it sits on the text
baseline. Hold a ref to call push for live values. See Sparklines.
Referential Stability
This is the one thing to get right, and it applies to every component in the package.
options and data props are compared by identity, not by value. A new object literal on
every render is a new identity, so the effect runs every render.
// Wrong: a new object every render, so applyOptions runs every render.
<FinancialChart options={{ theme: "dark", resolution: "1D" }} data={bars.map(toBar)} />
// Right: stable identities.
const OPTIONS = { theme: "dark", resolution: "1D" } as const; // module constant
const data = useMemo(() => rows.map(toBar), [rows]); // memoized
<FinancialChart options={OPTIONS} data={data} />
Callbacks are the exception. Every event prop and every shell callback is routed through a ref, so inline functions are correct and cost nothing.
Getting the Imperative Handle
import { useRef } from "react";
import { FinancialChart, type FinancialChartHandle } from "@ortex-charts/react";
const ref = useRef<FinancialChartHandle | null>(null);
<FinancialChart ref={ref} data={bars} onReady={(c) => c.chart.timeScale.fitContent()} />;
// Later:
ref.current?.chart.timeScale.setVisibleTimeRange(from, to);
ref.current?.alerts().add({ price: 182.5, label: "Breakout" });
onReady fires once with the handle after creation, which is the right place for anything
that has to happen before the user sees the first frame. The ref is the right place for
anything that happens later.
For realtime updates, prefer the handle over the data prop: ref.current?.update(bar) is
one row, while a new data array is a full setData.
Server-Side Rendering
The components check for window before creating anything, so they render an empty
container on the server and fill it on hydration. In Next.js the file still needs
"use client", because the effect only runs in a client component. There is no
server-rendered chart image; see Installation and delivery.
Re-Exported Types
To avoid importing from four packages, the React package re-exports the types callers usually need alongside the components:
import type {
ChartHandle, ChartOptions, DeepPartial, SeriesModel, SeriesDataInput, SeriesOptionsBase,
SeriesTypeMap, CrosshairEvent, VisibleRangeEvent,
FinancialChartHandle, FinancialChartOptions, SeriesMarker, LiveSource, LiveItem, StreamOptions,
AddIndicatorOptions, IndicatorInstance,
SparklineHandle, SparklineOptions, SparklineData,
ChartShellHandle, ChartShellOptions, ChartLayout, OverlayDef,
Bar,
} from "@ortex-charts/react";
What the Package Does Not Do
- There is no React renderer for the canvas. Children of
<Chart>are effects that call the imperative API; they do not reconcile drawing operations. - There are no components for drawings, events, order flow, replay or alerts. Those are
primitives on the chart, reached through the handle. Wrapping one in a component of your
own is about ten lines, as in the
SessionShadingexample above. <Series>does not manage realtime updates. Use theSeriesModelfromrefand callupdate, or hand the chart alivesource.