Put a live kandlo chart in a React app: first on your machine in about five minutes, then on your own domain.
How the pieces fit:
your app (app.example.com) chart page (charts.example.com) feed server (feed.example.com)
<KandloChart src=...> -- postMessage --> embed bridge + engine -- Socket.IO --> your market data
<iframe> <-- events ------- <-- candles, ticks --- The chart page is
apps/web/from this package: a static site. It runs in an<iframe>with its own React, its own global CSS and its own globals, so nothing in your app can break it and nothing in it leaks into your app. @kandlo/reactcreates that iframe and talks to it. It ships as a tarball in this package; it is not on a public package registry.- The feed server sends the chart its candles and ticks over Socket.IO. Serve your own market data shows how to serve your own.
Requirements: Node.js 18 or later for the kandlo tools, and a React 17+ app
(any build tool) served over http(s). Your app's build tool may need a newer
Node.js (current Vite needs 20.19 or later).
Part A: on your machine#
A1. Start a feed and the chart page#
In the kandlo folder:
npm install # once
node examples/custom-feed/server.mjs # terminal 1: example feed on ws://localhost:8443
npm run serve # terminal 2: chart page on http://localhost:9000Open http://localhost:9000/ and check that candles appear (EURUSD,
BTCUSD, XAUUSD and IDX500, a new tick every 500 ms). The built bundles
are part of the package, so no build step is needed.
Other feeds for local work:
npx cross-env FEED=otc npm run relay: an offline simulated market with 129 symbols. It also listens on port 8443, so stop the example feed first.- No feed at all: pass
params={{ replay: 1 }}to the component below and the chart plays a recorded EUR/USD session.
A2. Install the component#
Copy the tarball into your app's repository, so every checkout and CI run has it, and install it from the file:
mkdir -p vendor
cp <kandlo>/packages/kandlo-react/dist/kandlo-react-1.0.0.tgz vendor/
npm install ./vendor/kandlo-react-1.0.0.tgzpackage.json then records
"@kandlo/react": "file:vendor/kandlo-react-1.0.0.tgz". In a git checkout of
kandlo (rather than the release package), build the tarball first with
npm run pack:react.
A3. Render a chart#
import { useRef } from 'react';
import { KandloChart } from '@kandlo/react';
export function PriceChart() {
const chart = useRef(null); // the chart handle once it is ready
return (
<div style={{ height: 480 }}>
<KandloChart
ref={chart}
src="http://localhost:9000/"
symbol="EURUSD"
timeframe="M5"
chartType="Candles"
theme="dark-blue"
chrome="minimal"
onReady={(state) => console.log('chart ready', state.chart)}
onError={(err) => console.warn(err.code, err.message)}
/>
</div>
);
}Start your app's dev server and open it. The chart fills its container: give the container a height (without one it is 300 px tall).
Then try the handle:
await chart.current.addIndicator('rsi', { period: 21 });
await chart.current.setTimeframe('H1');
const candles = await chart.current.getCandles({ count: 100 });Checked for this guide: A2 and A3 run as written in a new Vite app (the
tarball installed with npm install, React 18), with the example feed: the
chart reported ready on EURUSD M5, then addIndicator, setTimeframe('H1')
and getCandles({ count: 100 }) all succeeded.
A complete app with symbol, timeframe, theme and layout selectors, an
indicator button and a crosshair readout is in
examples/react-vite/ (examples/react-vite/).
Server-rendered React apps#
<KandloChart/> can be imported from a server component (for example a page
in the Next.js App Router): @kandlo/react marks the component with
'use client', so the framework renders it on the client. On the server it
renders an empty <div data-kandlo-chart>; the iframe is created in the
browser after hydration. Nothing in the package reads window or document
at import time. Give the container a height in your server markup, as above.
Checked: a Next.js 15 App Router app (React 19) with <KandloChart/> in a
server component page built with next build, and the started app drew the
chart (getState reported ready on EURUSD M5).
If the chart does not appear, see Troubleshooting.
Part B: on your own domain#
The recommended setup uses three origins:
| Origin | Serves |
|---|---|
https://app.example.com |
your app |
https://charts.example.com |
the chart page (static files) |
wss://feed.example.com |
the feed server |
The chart page should have its own origin, different from your app's: the iframe sandbox only holds across origins. See Security.
B1. Configure the chart page#
Edit apps/web/index.html. Search for var AppData =: it is one long line
inside an inline <script>. Right after that statement (your script must run
before app.js), add:
<script>
// Origins allowed to embed and drive the chart.
AppData['embed-allowed-origins'] = ['https://app.example.com'];
// Your feed server. wss:// because the page is served over https.
AppData['relay-servers-list'] = [{ server: 'wss://feed.example.com', name: 'Primary' }];
</script>The time axis shows each viewer's local time. To show one zone to everyone,
add AppData['userTimeZoneOffset'] = 0; (UTC; the value is minutes east of
UTC) to the same script. Keep serverTimeZoneOffset at 0, as shipped; see
API reference.
B2. Build the static site#
npm run build:static # writes dist/dist/ is a self-contained static site (about 11 MB): index.html,
assets/, kandlo-assets/ and favicon.ico. The script stamps the bundle
version into index.html and reports any asset path that would return 404.
It is the same index.html you edited in B1; you can also edit
dist/index.html after the build.
Rebuild the bundles first (npm run build && npm run build:interactions) only
if you changed the chart's source code or themes.
B3. Upload it#
Serve dist/ from https://charts.example.com/ with any static host or
object store. Response headers:
| Path | Header |
|---|---|
index.html |
Cache-Control: no-cache (it carries the bundle version stamp) |
index.html |
Content-Security-Policy: frame-ancestors https://app.example.com (who may frame the chart; see Security) |
| everything else | normal caching |
Do not send X-Frame-Options: DENY or SAMEORIGIN on index.html; either
one blocks the embed.
Checked for this guide: dist/ served by a plain static file server (no
rewriting, no fallbacks) with a frame-ancestors header, framed by another
origin, drew 1000 H1 candles from the example feed with no 404 and no console
error.
B4. Run the feed#
Deploy your feed server (Serve your own market data) behind TLS
at feed.example.com, and limit it to the chart page's origin. With the
example server:
PORT=8443 FEED_ALLOWED_ORIGINS=https://charts.example.com node examples/custom-feed/server.mjsTLS termination (for wss://) is your proxy's or platform's job; the example
listens on plain ws. The VAR=value command form is for POSIX shells; on
Windows use npx cross-env PORT=8443 FEED_ALLOWED_ORIGINS=... node examples/custom-feed/server.mjs.
B5. Point your app at it#
<KandloChart src="https://charts.example.com/" symbol="EURUSD" timeframe="M5" />Without React#
The same package works in any page through the plain API, which does not import React:
import { createKandloChart } from '@kandlo/react/vanilla';
const chart = await createKandloChart(document.getElementById('chart'), {
src: 'https://charts.example.com/',
symbol: 'BTCUSD',
timeframe: 'H1',
chrome: 'minimal',
});
chart.on('candle', (e) => console.log(e.candle.close, e.isNew));
await chart.addIndicator('ma', { period: 50 });
// later: chart.destroy();Without any npm package, post the messages yourself; the protocol is in API reference.
Troubleshooting#
Look in two consoles: your app's, and the chart page's (in the browser's
developer tools, select the chart frame in the console's context selector).
The chart page reports its own problems with an [embed] prefix.
| What you see | Cause and fix |
|---|---|
| The chart is only 300 px tall | The container has no height of its own, so the component's minimum applies. Give it, or its parent, a height. |
onError with timeout, "no answer from the chart page" |
src is wrong, the chart page is not running, or your app's origin is not allowed. The chart console then says [embed] bridge disabled: <reason>; add your origin to embed-allowed-origins (B1). |
timeout, "answered but did not report ready" |
The chart page runs but never got going. If its console says [embed] the chart has not started yet; still waiting, it has not reached the feed: check the feed is running and that the URL in relay-servers-list is reachable from the browser. The page keeps waiting and reports ready once the feed connects, but createKandloChart and <KandloChart> give up at readyTimeout; remount the chart after fixing the feed. |
onReady fires, but state.hasData is false |
The feed connected but sent no candles within 30 seconds for that symbol and timeframe. See the checklist. |
bad_option, "the host page has no http(s) origin" |
Your page was opened from file://. Serve it over http. |
The chart shows another symbol than symbol |
The symbol is not in the feed's list. The chart console says [embed] ignored ?symbol=: unknown symbol: .... |
| The browser console says it refused to frame the page | The chart page's Content-Security-Policy: frame-ancestors does not list your app, or it sends X-Frame-Options (B3). |
| It works locally, not deployed | An https page cannot open ws://: use wss:// in relay-servers-list (B1), and allow the chart page's origin on the feed (B4). |
A fix in index.html has no effect |
index.html is cached. Serve it with Cache-Control: no-cache (B3) and reload. |
FAQ#
Is @kandlo/react on a public package registry? No. It ships as a tarball
in this package and is installed from a file (A2).
Do I need React? No. @kandlo/react/vanilla works in any page, and the
postMessage protocol works
with no package at all.
Do I need a server? Two static sites (your app and the chart page) and a
feed server. For a demo without a feed, pass params={{ replay: 1 }}.
Can the chart page share my app's origin? It works, but the iframe sandbox no longer protects your app. Use a separate origin, even a subdomain (Security).
Can I change the feed URL from my app? No. It is read once from the chart
page's index.html (Serve your own market data).
Do indicators and drawings survive a reload? No. Re-apply them after
ready; the component's indicators prop does it for you.
Can I put several charts on one page? Yes. Each <KandloChart/> is its own
iframe with its own feed connection.
Which time zone does the axis show? Each viewer's local time, unless
AppData['userTimeZoneOffset'] sets one (B1).
Are TypeScript types included? Yes, for both entry points (API reference).
Next#
- Serve your own market data: serve candles and ticks from your database
- API reference: every option, method, event and error code
- Mobile apps (web views): the chart in a native app
- Theming: themes, and making your own
- Security: origins, sandbox, headers, the feed