Skip to main content

TradeScript Widget

Mounted TradeScript chart widget with live candles, legend, scales, and chart controls
Expected result after readiness: the requested symbol has rendered bars and the enabled chart controls are interactive.

The chart widget is the production chart surface. Its lifecycle has three phases, identical in React and imperative hosts — only the syntax differs.

Lifecycle: mount, ready, cleanup

  1. Mount. The widget claims a DOM element, renders its shell, and starts initializing the chart surface. The API object exists immediately, but data accessors are not safe yet.
  2. Ready. Once the surface is initialized and the first symbol resolution has started, ready() resolves. In React, onMount(mounted) provides the same handle so the application can await mounted.ready(). Only after this point should you read loaded bars or call controllers.
  3. Cleanup. destroy() (imperative) or unmounting the component (React) tears down the chart, cancels datafeed subscriptions, and releases the DOM. A destroyed widget must not be reused.

Errors during mount surface through the onError callback as a ChartError with code: 'widget.mount'; failures during initialization reject the ready() promise.

React or imperative?

  • Use TradeScriptWidget from @tradescript/pro/react when the host is a React app. It is a thin adapter over the core lifecycle: React owns its one container element and unmount; core owns the chart.
  • Use sdk.chart.mount(...) from the authorization-bound core SDK in every framework when you want direct lifecycle ownership. Its handle owns api, ready(), update(), and destroy(). Create the shared SDK once before the first chart is mounted, then pass it to every chart surface owned by the application.

Minimal example: mount, observe ready, destroy

The imperative path shows the whole lifecycle in one place:

import { createTradeScriptSdk } from '@tradescript/pro/sdk/core';

const sdk = await createTradeScriptSdk({ lease: deploymentLease });

const mounted = sdk.chart.mount({
mount: '#chart',
symbol: 'NASDAQ:AAPL',
interval: '1D',
datafeed,
});

// Observable ready state: the promise resolves only when the API is safe to use.
await mounted.ready();
console.log('chart ready', mounted.api.chart().getSymbol().ticker);

// Cleanup: releases the DOM, subscriptions, and controllers.
mounted.destroy();

Verify the lifecycle: before ready() resolves the mount element contains the loading surface; after it resolves chart.getSymbol() returns the resolved symbol; after destroy() the mount element is empty and no further datafeed requests are issued.

The same lifecycle in React — mount and cleanup are owned by the component:

import { TradeScriptWidget } from '@tradescript/pro/react';
import { createTradeScriptSdk } from '@tradescript/pro/sdk/core';

const sdk = await createTradeScriptSdk({ lease: deploymentLease });

export function CustomerChart() {
return (
<TradeScriptWidget
sdk={sdk}
symbol={{ ticker: 'AAPL' }}
interval="1D"
datafeed={datafeed}
theme="dark"
features={{ drawings: true, indicators: true }}
onMount={(mounted) => {
void mounted.ready().then(() => {
console.log('chart ready', mounted.api.chart().getSymbol().ticker);
});
}}
/>
);
}

Unmounting the component performs the destroy() step. Call sdk.close() only when the application no longer needs any surface owned by that customer deployment, and sdk.replaceLease(nextLease) only when a long-running page receives a newer backend-cached lease.

Optional custom element

The element adapter is opt-in and has no registration side effect. Define it once, provide core widget options, and remove it normally when the host route unmounts:

import { createTradeScriptSdk } from '@tradescript/pro/sdk/core';
import { defineTradeScriptChartElement } from '@tradescript/pro/sdk/element';

const sdk = await createTradeScriptSdk({ lease: deploymentLease });
const ChartElement = defineTradeScriptChartElement(sdk);
const chart = new ChartElement();
chart.options = {
symbol: 'AAPL',
interval: '1D',
datafeed,
theme: 'dark',
};
document.querySelector('#chart-host')?.append(chart);
await chart.ready();

The element delegates connection, updates, readiness, and disconnection to the same core mount handle. It uses light DOM so the published SDK styles apply without a second styling boundary.

Intervals

interval sets the initial interval. The available interval buttons come from datafeed.onReady().supportedIntervals, with the SDK default list used when no list is declared.

const datafeed: MarketDataFeed = {
onReady: () => ({
supportedIntervals: ['1T', '10T', '1m', '6m', '2D', '3W', '6M', '1Y'],
providedIntervals: ['1T', '10T', '1m', '2D', '1M'],
resolutionRebuildPolicy: 'aggregate',
}),
loadBars,
};

Exact vendor bars are the default. Use resolutionRebuildPolicy: 'aggregate' or createCachingDatafeed(rawFeed, { syntheticAggregation: true }) when supportedIntervals includes intervals the raw feed does not return directly and the SDK default base tier is acceptable. Use sourceIntervals for explicit rules such as { '10m': '5m', '100T': '10T' }. Calendar bars follow stricter rules: W/M/Y can build from daily bars when vendor W/M/Y bars are absent, but D/W/M/Y are not built from intraday/hourly/tick bars.

The interval selector also accepts typed custom intervals. A typed value is selectable only when it exists in the resolved supportedIntervals; valid unsupported entries are shown disabled. Set features={{ customIntervals: false }} to hide the typed entry.

For exchange-traded symbols, implement resolveSessionCalendar or pass syntheticAggregation.sessionCalendar so synthetic bars align to sessions instead of fixed UTC buckets. Set emptyBars: true on the caching datafeed or feed config when missing intraday session slots should be filled with flat SDK bars.

Next steps