Docs·Charting Library·Guides

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:

  1. Call prefetchEngine() at the app root, not in the chart component. Earlier = faster first paint.
  2. Always call chart.destroy() on unmount. Forgetting this leaks ~5–20 MB of engine memory per unmounted component.
  3. Wire a ResizeObserver once so the chart resizes when its container changes size.

React

JSX
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.

JSX
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)

Vue
<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).
Vue
<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.

JavaScript
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 but visibility: 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 a useEffect that depends on every prop will paint up to 60× per second on prop churn. The engine already redraws on setX calls — 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.