React & Vue Integration
Mount the chart inside a React or Vue component without leaking event listeners or memory. Includes resize handling, theme sync, and graceful unmount.
kline-orderbook-chart is framework-agnostic — it just needs a <canvas> DOM node and the data. This page shows the canonical mount-and-cleanup recipes for React and Vue 3.
The recipes have three things in common, which are also the three things every framework integration must get right:
- Call
prefetchEngine()at the app root, not in the chart component. Earlier = faster first paint. - Always call
chart.destroy()on unmount. Forgetting this leaks ~5–20 MB of engine memory per unmounted component. - Wire a
ResizeObserveronce so the chart resizes when its container changes size.
React
import { useEffect, useRef } from 'react'
import { createChartBridge, prefetchEngine } from '@mrd/chart-engine'
// Call this once, at the top of your app (e.g. _app.tsx or main.tsx).
// Subsequent createChartBridge calls reuse the prefetched module.
prefetchEngine()
export function TradingChart({ licenseKey, symbol, interval = '1h' }) {
const containerRef = useRef(null)
const canvasRef = useRef(null)
const chartRef = useRef(null)
useEffect(() => {
let chart
let resizeObserver
let cancelled = false
;(async () => {
const canvas = canvasRef.current
if (!canvas) return
chart = await createChartBridge(canvas, { licenseKey })
if (cancelled) {
chart.destroy()
return
}
chartRef.current = chart
// Initial config
chart.setTheme('dark')
chart.setPrecision(2)
chart.setCandleInterval(intervalToSeconds(interval))
// Load data
const klines = await fetchKlines(symbol, interval, 1000)
chart.setKlines(
klines.timestamps,
klines.opens,
klines.highs,
klines.lows,
klines.closes,
klines.volumes,
)
// Start render loop
chart.start()
// Watch container size
resizeObserver = new ResizeObserver(() => {
chart.resize()
})
resizeObserver.observe(containerRef.current)
})()
return () => {
cancelled = true
resizeObserver?.disconnect()
chartRef.current?.destroy()
chartRef.current = null
}
}, [licenseKey, symbol, interval])
return (
<div ref={containerRef} style={{ width: '100%', height: '600px' }}>
<canvas
ref={canvasRef}
style={{ width: '100%', height: '100%', display: 'block' }}
/>
</div>
)
}
React: connecting real-time data
Add a websocket effect that calls chart.updateLastKline() / chart.appendKline() as ticks arrive. Run it on a separate effect so a websocket reconnect doesn't tear down the chart.
useEffect(() => {
const chart = chartRef.current
if (!chart) return
const ws = new WebSocket(/* … */)
ws.onmessage = (ev) => {
const tick = JSON.parse(ev.data)
if (tick.barClosed) {
chart.appendKline(tick.ts, tick.o, tick.o, tick.o, tick.o, 0)
} else {
chart.updateLastKline(tick.ts, tick.o, tick.h, tick.l, tick.c, tick.v)
}
}
return () => ws.close()
}, [symbol, interval])
React: avoiding the StrictMode double-mount gotcha
In dev mode, React StrictMode mounts every effect twice. The chart instance from the first mount gets destroyed in the cleanup — that's exactly what you want, no extra handling needed. The recipe above already handles this via the cancelled flag.
What you do NOT want is to call createChartBridge directly inside the render function — that would create a new chart every render, with no chance to clean up. Always wrap chart creation in an effect.
Vue 3 (Composition API)
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { createChartBridge, prefetchEngine } from '@mrd/chart-engine'
const props = defineProps({
licenseKey: { type: String, required: true },
symbol: { type: String, required: true },
interval: { type: String, default: '1h' },
})
const containerRef = ref(null)
const canvasRef = ref(null)
let chart = null
let resizeObserver = null
onMounted(async () => {
prefetchEngine() // safe to call even though we already created the bridge
chart = await createChartBridge(canvasRef.value, {
licenseKey: props.licenseKey,
})
chart.setTheme('dark')
chart.setPrecision(2)
chart.setCandleInterval(intervalToSeconds(props.interval))
const klines = await fetchKlines(props.symbol, props.interval, 1000)
chart.setKlines(
klines.timestamps,
klines.opens, klines.highs, klines.lows, klines.closes, klines.volumes,
)
chart.start()
resizeObserver = new ResizeObserver(() => chart.resize())
resizeObserver.observe(containerRef.value)
})
onBeforeUnmount(() => {
resizeObserver?.disconnect()
chart?.destroy()
chart = null
})
</script>
<template>
<div ref="containerRef" class="chart-container">
<canvas ref="canvasRef" class="chart-canvas" />
</div>
</template>
<style scoped>
.chart-container {
width: 100%;
height: 600px;
}
.chart-canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
Vue: reacting to prop changes
When props change, you typically want to either:
- Reload the data only (cheap) — call
chart.setKlines(...)again with the new symbol's bars. - Recreate the chart entirely (expensive, only needed for theme or major chart-type changes).
<script setup>
import { watch } from 'vue'
watch(() => [props.symbol, props.interval], async ([sym, ivl]) => {
if (!chart) return
const klines = await fetchKlines(sym, ivl, 1000)
chart.setCandleInterval(intervalToSeconds(ivl))
chart.setKlines(
klines.timestamps,
klines.opens, klines.highs, klines.lows, klines.closes, klines.volumes,
)
})
</script>
The chart preserves its viewport across setKlines reloads when you use setKlinesPreserveViewport instead — useful for the "user is studying bars at 12:00 and switched interval" case.
Svelte / Vanilla
The recipe is shorter because there's no component lifecycle to fight with. Mount the canvas in onMount, destroy in onDestroy.
import { onMount, onDestroy } from 'svelte'
import { createChartBridge, prefetchEngine } from '@mrd/chart-engine'
prefetchEngine()
let canvas
let chart
let ro
onMount(async () => {
chart = await createChartBridge(canvas, { licenseKey })
chart.setTheme('dark')
// ... load data
chart.start()
ro = new ResizeObserver(() => chart.resize())
ro.observe(canvas.parentElement)
})
onDestroy(() => {
ro?.disconnect()
chart?.destroy()
})
Performance tips for any framework
- Don't render the chart in a hidden parent. If your tab system uses
display: none, the engine cannot determine the canvas size and will skip its initial resize. Either keep the canvas mounted butvisibility: hidden, or destroy + recreate on tab change. - Throttle prop changes. If your symbol picker fires a prop change on every keystroke, you'll thrash
setKlines. Debounce the input or only re-fetch on enter / blur. - One chart per page. The engine is fast enough to mount multiple charts (5–10 small ones), but the canvas pixel budget adds up. For dashboard-style "wall of charts" UIs, lazy-mount the offscreen ones.
- Don't fight the engine's render scheduling. Calling
chart.renderFrameNow()from auseEffectthat depends on every prop will paint up to 60× per second on prop churn. The engine already redraws onsetXcalls — don't double up.
Common pitfalls
Chart appears blank in production but works in dev.
Your bundler is splitting the engine module to a separate chunk that isn't pre-loaded. Either: import the package the same way in both environments, or add a <link rel="modulepreload"> for the chunk.
Chart works but events fire on the wrong element.
You have a pointer-events: none overlay (or similar) between the canvas and the user. The engine attaches its listeners to the canvas — make sure clicks actually land on it.
Chart re-renders on every parent re-render.
You're passing a new licenseKey object on every render (likely from a const declared in the parent's body). Pull the key out into a stable reference or memoise.
Next
- Data Loading — the real-time tick / append patterns.
- Indicators — turn on RSI / MACD / VRVP from within your component.
- Chart Instance — every lifecycle method you'll touch from a framework.