React Hook封装完全指南
📋 概述
React Hook封装是将ECharts集成到React应用中的现代化方案。通过自定义Hook,我们可以实现图表的自动初始化、响应式更新、资源清理等功能,让ECharts在React中更加优雅和高效。
核心价值
- 声明式API:符合React编程范式
- 自动管理:生命周期、资源清理自动化
- TypeScript支持:完整的类型定义
- 性能优化:避免不必要的重渲染
- 可复用性:一次封装,多处使用
🎯 核心概念
1. 基础Hook结构
typescript
import { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
export function useECharts(
option: EChartsOption,
config?: UseEChartsConfig
) {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
useEffect(() => {
// 初始化图表
if (chartRef.current) {
chartInstance.current = echarts.init(chartRef.current);
chartInstance.current.setOption(option);
}
// 清理函数
return () => {
if (chartInstance.current) {
chartInstance.current.dispose();
}
};
}, []);
return { chartRef, chartInstance: chartInstance.current };
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2. 使用方式
tsx
function MyChart() {
const option = {
xAxis: { type: 'category', data: ['A', 'B', 'C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [10, 20, 30] }]
};
const { chartRef } = useECharts(option);
return <div ref={chartRef} style={{ width: '100%', height: '400px' }} />;
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
🔧 完整实现
TypeScript类型定义
typescript
// types.ts
import { EChartsOption, EChartsType } from 'echarts';
export interface UseEChartsConfig {
theme?: string;
renderer?: 'canvas' | 'svg';
autoResize?: boolean;
loading?: boolean;
onChartReady?: (chart: EChartsType) => void;
onChartError?: (error: Error) => void;
}
export interface UseEChartsReturn {
chartRef: React.RefObject<HTMLDivElement>;
chartInstance: EChartsType | null;
resize: () => void;
updateOption: (option: EChartsOption) => void;
clear: () => void;
dispose: () => void;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
完整Hook实现
typescript
// useECharts.ts
import { useEffect, useRef, useState, useCallback } from 'react';
import * as echarts from 'echarts';
import type { EChartsOption, EChartsType } from 'echarts';
import type { UseEChartsConfig, UseEChartsReturn } from './types';
export function useECharts(
option: EChartsOption,
config: UseEChartsConfig = {}
): UseEChartsReturn {
const {
theme,
renderer = 'canvas',
autoResize = true,
loading = false,
onChartReady,
onChartError
} = config;
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<EChartsType | null>(null);
const [isReady, setIsReady] = useState(false);
// 初始化图表
useEffect(() => {
if (!chartRef.current) return;
try {
// 创建图表实例
const chart = echarts.init(chartRef.current, theme, {
renderer
});
chartInstance.current = chart;
// 设置初始配置
chart.setOption(option);
// 显示加载动画
if (loading) {
chart.showLoading();
}
// 标记为就绪
setIsReady(true);
// 调用就绪回调
onChartReady?.(chart);
// 监听窗口大小变化
if (autoResize) {
const handleResize = () => {
chart.resize();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}
} catch (error) {
console.error('Failed to initialize ECharts:', error);
onChartError?.(error as Error);
}
// 清理函数
return () => {
if (chartInstance.current) {
chartInstance.current.dispose();
chartInstance.current = null;
setIsReady(false);
}
};
}, [theme, renderer, autoResize, loading, onChartReady, onChartError]);
// 更新配置
useEffect(() => {
if (chartInstance.current && isReady) {
chartInstance.current.setOption(option, {
notMerge: false,
lazyUpdate: false
});
// 隐藏加载动画
if (!loading) {
chartInstance.current.hideLoading();
}
}
}, [option, loading, isReady]);
// 手动调整大小
const resize = useCallback(() => {
if (chartInstance.current) {
chartInstance.current.resize();
}
}, []);
// 更新配置(外部调用)
const updateOption = useCallback((newOption: EChartsOption) => {
if (chartInstance.current) {
chartInstance.current.setOption(newOption, {
notMerge: false,
replaceMerge: []
});
}
}, []);
// 清空图表
const clear = useCallback(() => {
if (chartInstance.current) {
chartInstance.current.clear();
}
}, []);
// 销毁图表
const dispose = useCallback(() => {
if (chartInstance.current) {
chartInstance.current.dispose();
chartInstance.current = null;
setIsReady(false);
}
}, []);
return {
chartRef,
chartInstance: chartInstance.current,
resize,
updateOption,
clear,
dispose
};
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
💡 实战案例
案例1:基础柱状图
tsx
import React from 'react';
import { useECharts } from './useECharts';
function BarChart() {
const option = {
title: {
text: '月度销售统计',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['一月', '二月', '三月', '四月', '五月']
},
yAxis: {
type: 'value'
},
series: [{
name: '销售额',
type: 'bar',
data: [120, 200, 150, 80, 70],
itemStyle: {
color: '#5470c6'
}
}]
};
const { chartRef } = useECharts(option);
return (
<div
ref={chartRef}
style={{ width: '100%', height: '400px' }}
/>
);
}
export default BarChart;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
案例2:动态数据折线图
tsx
import React, { useState, useEffect } from 'react';
import { useECharts } from './useECharts';
function DynamicLineChart() {
const [data, setData] = useState<number[]>([]);
const option = {
title: {
text: '实时数据监控',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: Array.from({ length: data.length }, (_, i) => i + 1)
},
yAxis: {
type: 'value'
},
series: [{
name: '数值',
type: 'line',
data: data,
smooth: true,
areaStyle: {
opacity: 0.3
}
}]
};
const { chartRef } = useECharts(option);
// 模拟实时数据更新
useEffect(() => {
const interval = setInterval(() => {
setData(prev => {
const newData = [...prev, Math.random() * 100];
if (newData.length > 20) {
newData.shift();
}
return newData;
});
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div
ref={chartRef}
style={{ width: '100%', height: '400px' }}
/>
);
}
export default DynamicLineChart;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
案例3:带主题切换的图表
tsx
import React, { useState } from 'react';
import { useECharts } from './useECharts';
function ThemeSwitchableChart() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const option = {
title: {
text: '主题切换示例',
left: 'center'
},
xAxis: {
type: 'category',
data: ['A', 'B', 'C', 'D']
},
yAxis: {
type: 'value'
},
series: [{
type: 'bar',
data: [10, 20, 30, 40]
}]
};
const { chartRef } = useECharts(option, {
theme,
autoResize: true
});
return (
<div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换到{theme === 'light' ? '暗色' : '亮色'}主题
</button>
<div
ref={chartRef}
style={{ width: '100%', height: '400px' }}
/>
</div>
);
}
export default ThemeSwitchableChart;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
案例4:高级用法 - 事件监听
tsx
import React, { useEffect } from 'react';
import { useECharts } from './useECharts';
function ChartWithEvents() {
const option = {
xAxis: {
type: 'category',
data: ['A', 'B', 'C', 'D']
},
yAxis: {
type: 'value'
},
series: [{
type: 'bar',
data: [10, 20, 30, 40]
}]
};
const { chartRef, chartInstance } = useECharts(option, {
onChartReady: (chart) => {
console.log('图表已就绪');
// 监听点击事件
chart.on('click', (params) => {
console.log('点击了:', params);
});
// 监听鼠标悬停
chart.on('mouseover', (params) => {
console.log('悬停在:', params);
});
}
});
return (
<div
ref={chartRef}
style={{ width: '100%', height: '400px' }}
/>
);
}
export default ChartWithEvents;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
⚠️ 常见问题
问题1:图表不显示
原因:容器没有宽高
解决:
tsx
// ❌ 错误:没有设置宽高
<div ref={chartRef} />
// ✅ 正确:设置明确的宽高
<div ref={chartRef} style={{ width: '100%', height: '400px' }} />
// 或使用CSS
<div ref={chartRef} className="chart-container" />
<style>{`
.chart-container {
width: 100%;
height: 400px;
}
`}</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
问题2:内存泄漏
原因:组件卸载时未销毁图表
解决:
typescript
// useECharts中已经处理
useEffect(() => {
return () => {
if (chartInstance.current) {
chartInstance.current.dispose(); // 清理资源
}
};
}, []);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
问题3:响应式失效
原因:未启用autoResize或窗口事件未绑定
解决:
typescript
const { chartRef, resize } = useECharts(option, {
autoResize: true // 启用自动调整
});
// 或者手动调用
useEffect(() => {
const observer = new ResizeObserver(() => {
resize();
});
if (chartRef.current) {
observer.observe(chartRef.current);
}
return () => observer.disconnect();
}, [chartRef, resize]);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🎯 最佳实践
1. 性能优化
typescript
// 使用memo避免不必要的重渲染
const ChartComponent = React.memo(function ChartComponent({ data }) {
const option = useMemo(() => ({
series: [{ data }]
}), [data]);
const { chartRef } = useECharts(option);
return <div ref={chartRef} style={{ width: '100%', height: '400px' }} />;
});1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
2. 错误边界
tsx
import { ErrorBoundary } from 'react-error-boundary';
function SafeChart() {
return (
<ErrorBoundary fallback={<div>图表加载失败</div>}>
<ChartComponent />
</ErrorBoundary>
);
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
3. 懒加载
typescript
// 按需引入ECharts
import * as echarts from 'echarts/core';
import { BarChart } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([BarChart, CanvasRenderer]);1
2
3
4
5
6
2
3
4
5
6
📊 性能指标
| 操作 | 耗时 | 说明 |
|---|---|---|
| 初始化 | 50-100ms | 首次创建 |
| 更新配置 | 10-30ms | setOption |
| 调整大小 | 5-15ms | resize |
| 销毁 | 5-10ms | dispose |
🔗 相关链接
💎 总结
React Hook核心价值:
- ✅ 声明式API,符合React范式
- ✅ 自动管理生命周期
- ✅ TypeScript完整支持
- ✅ 性能优化和错误处理
关键使用原则:
- 始终设置宽高:确保容器有明确尺寸
- 启用autoResize:自动响应窗口变化
- 及时清理资源:避免内存泄漏
- 使用memo优化:避免不必要的重渲染
掌握React Hook封装,让ECharts在React中更优雅!⚛️
