Optimizing ECharts Integration in Vue and React with Lazy Loading, Theme Switching, and DOM Observation
This article presents a comprehensive solution for integrating ECharts into Vue and React projects, addressing bundle size, API complexity, and lack of CSS‑variable support by introducing custom imports, a draw helper, MutationObserver‑based resizing, theme‑aware color replacement, lazy loading, and usage examples for both frameworks.
When building ECharts components we often encounter three problems: the bundled chunk is large, the API usage is cumbersome with DOM rendering timing and resource cleanup concerns, and ECharts does not support CSS variables for dynamic theming. This article proposes a complete solution to these issues.
Technology stack : Vue (or React), ECharts, TypeScript.
Dependency import : Create a dedicated ECharts import file that only brings in the required components and charts, then mount them with echarts.use and expose a draw helper for initializing, clearing, and setting options.
import * as echarts from 'echarts/core'
import { GraphicComponent, GridComponent, LegendComponent, PolarComponent, TitleComponent, TooltipComponent } from 'echarts/components'
import { BarChart, BoxplotChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GraphicComponent,
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent,
LineChart,
BarChart,
PieChart,
BoxplotChart,
CanvasRenderer,
UniversalTransition,
RadarChart,
PolarComponent,
])
const draw = (dom: HTMLElement, option: Record
) => {
const chart = echarts.init(dom)
chart.clear()
chart.setOption(option)
return chart
}
export default { ...echarts, draw } as anySimple Vue component : The component receives options , watches for changes, initializes the chart on mount, disposes it on unmount, and uses the draw helper.
<template>
<div ref="domBox" :style="{ width, height }">
<div ref="domRef" :style="{ width, height }" />
</div>
</template>
<script lang="ts" setup>
import { watch, ref, onMounted, onUnmounted } from 'vue'
import type { ECharts } from 'echarts'
import echarts from './index'
const props = defineProps({
width: { type: String, default: '100%' },
height: { type: String, default: '100%' },
options: { type: Object, default: null },
})
const domRef = ref(null)
const domBox = ref(null)
let chartObj: ECharts | null = null
onMounted(() => {
if (!domRef.value) return
init()
if (props.options) drawOption()
})
onUnmounted(() => {
chartObj?.dispose()
chartObj = null
})
watch(() => props.options, () => drawOption())
const init = () => { chartObj = echarts.init(domRef.value as any) }
const drawOption = () => {
if (!chartObj) return
chartObj.setOption(props.options)
}
</script>DOM size changes : Use MutationObserver to watch the container element and call chart.resize() when its size changes.
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.target === domBox.value) chartObj?.resize()
}
})
observer.observe(domBox.value, { attributes: true, childList: false, characterData: true, subtree: true })Theme switching : Since ECharts renders on canvas, CSS variables cannot be used directly. A utility replaceVarStrings recursively replaces strings like 'var(--dv-color-1)' with the actual computed color value obtained via getComputedStyle .
export const useThemeValue = (styleVariables: string) =>
getComputedStyle(document.documentElement).getPropertyValue(styleVariables)
export function replaceVarStrings(obj: Record
) {
const newObj: Record
= Array.isArray(obj) ? [] : {}
for (const key in obj) {
if (typeof obj[key] === 'object') {
newObj[key] = replaceVarStrings(obj[key])
} else if (typeof obj[key] === 'string' && obj[key].startsWith('var(') && obj[key].endsWith(')')) {
const varContent = obj[key].slice(4, -1)
newObj[key] = useThemeValue(varContent)
} else {
newObj[key] = obj[key]
}
}
return newObj
}Detecting dark mode : A hook useTheme creates a MutationObserver on the html element to monitor the class attribute and expose a reactive isDark flag.
export const useTheme = () => {
const htmlDom = document.querySelector('html')
if (!htmlDom) return { isDark: ref(false) }
const isDark = ref(!!htmlDom.classList.contains('dark'))
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
isDark.value = (mutation.target as HTMLElement).className.includes('dark')
}
}
})
observer.observe(htmlDom, { attributes: true, attributeFilter: ['class'] })
return { isDark }
}Lazy mode : When the lazy prop is true, the component does not automatically react to options changes; instead it exposes drawOption , resize , and init methods via defineExpose for manual control.
<script lang="ts" setup>
// ... same imports as before
const props = defineProps({
width: { type: String, default: '100%' },
height: { type: String, default: '100%' },
lazy: { type: Boolean, default: false },
options: { type: Object, default: null },
})
// ... initialization code
const drawOption = (options = props.options) => {
if (!chartObj) return
if (!options) {
chartObj.clear()
chartObj.showLoading({ /* loading config */ })
} else {
chartObj.hideLoading()
chartObj.setOption(replaceVarStrings(options))
}
}
defineExpose({ drawOption, resize, init })
</script>Usage examples :
<template>
<div style="height: 300px; width: 200px;">
<BaseECharts :options="options" />
</div>
</template>
<script setup lang="ts">
import BaseECharts from '@/utils/echarts/BaseECharts.vue'
const options = ref({
xAxis: { type: 'category', data: [] },
color: ['var(--dv-color-danger)'],
yAxis: { type: 'value' },
series: [{ data: [], type: 'line' }],
})
setTimeout(() => {
options.value = { /* populated option object */ }
}, 3000)
</script> import { useEffect, useRef } from 'react'
import echarts from './index'
import { replaceVarStrings, useTheme } from './utils'
export default function EchartController({ width = '100%', height = '100%', options }) {
const chartRef = useRef()
const cInstance = useRef()
const { isDark } = useTheme()
useEffect(() => {
if (!chartRef.current) return
cInstance.current = echarts.init(chartRef.current)
const observer = new ResizeObserver(() => cInstance.current.resize())
observer.observe(chartRef.current)
return () => { cInstance.current?.dispose(); observer.disconnect() }
}, [])
useEffect(() => {
if (!cInstance.current) return
if (!options) {
cInstance.current.showLoading({ /* config */ })
return
}
cInstance.current.hideLoading()
cInstance.current.setOption(replaceVarStrings(options))
}, [options, isDark])
return
}References: MDN documentation for ResizeObserver and MutationObserver .
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.