用 ECharts + React 打造环形饼图:从 0 到 1 入门
这篇入门文会带你用"正确姿势"在 React 中接入 ECharts,完成一个可复用、可维护、可扩展的环形饼图组件,并解释关键的生命周期、类型与性能要点。示例代码参考 Demo.tsx。
为什么选 ECharts
- 能力强:内置几十种图表与丰富交互。
 - 生态成熟:社区活跃,示例与文档完善。
 - 高可定制:主题、动画、标签、交互都可细调。
 
环境准备
- 安装依赖:
 
            
            
              bash
              
              
            
          
          # 二选一
npm i echarts
pnpm add echarts
        - 建议使用 TypeScript:更安全的参数与事件类型提示。
 
组件结构与关键思路
- 
DOM 容器与实例引用
- 用 
useRef<HTMLDivElement>(null)拿到真实 DOM 容器。 - 用 
useRef<echarts.ECharts | null>(null)存放 ECharts 实例,便于resize()/dispose()。 
 - 用 
 - 
受控数据与类型
- 统一数据结构,利于类型提示与后续扩展:
 
 
            
            
              ts
              
              
            
          
          interface ChartData {
  value: number;
  name: string;
}
        - 生命周期管理(核心)
- 在 
useEffect初始化并渲染图表。 - 绑定 
window.resize做自适应。 - 返回清理函数移除监听并 
dispose(),避免内存泄漏。 - 以数据 
chartData为依赖,数据变更自动刷新图表。 
 - 在 
 
手把手实现要点
- 初始化实例:
 
            
            
              ts
              
              
            
          
          const chartInstance = echarts.init(chartRef.current!);
chartInstanceRef.current = chartInstance;
        - 配置 Option(标题/提示框/图例/系列/颜色/动画):
- 标题与副标题:
title - 提示框:
tooltip,用"运行时守卫"写法规避类型不匹配 - 图例:
legend.data = chartData.map(i => i.name) - 系列:
type: 'pie'+radius: ['40%', '70%']实现环形 - 颜色与动画:
color、animation* 
 - 标题与副标题:
 
            
            
              ts
              
              
            
          
          chartInstance.setOption(option);
        - 自适应与清理:
 
            
            
              ts
              
              
            
          
          const handleResize = () => {
  if (chartInstance && !chartInstance.isDisposed()) {
    chartInstance.resize();
  }
};
window.addEventListener('resize', handleResize);
return () => {
  window.removeEventListener('resize', handleResize);
  if (chartInstance && !chartInstance.isDisposed()) {
    chartInstance.dispose();
  }
  chartInstanceRef.current = null;
};
        在页面中使用
            
            
              tsx
              
              
            
          
          import Demo from './components/Demo';
export default function Page() {
  return (
    <div style={{ height: '100vh' }}>
      <Demo />
    </div>
  );
}
        换成你的真实数据
- 将 
useState<ChartData[]>(...)的 mock 数据替换为接口数据。 - 常见写法:
 
            
            
              ts
              
              
            
          
          const [chartData, setChartData] = useState<ChartData[]>([]);
useEffect(() => {
  let cancelled = false;
  fetch('/api/metrics')
    .then(res => res.json())
    .then(data => {
      if (!cancelled) setChartData(data);
    });
  return () => { cancelled = true; };
}, []);
        - 因为 
useEffect([...])依赖了chartData,数据更新会自动触发图表刷新。 
常见问题与排查
- 容器尺寸为 0,图表不显示
确保容器有明确宽高(如800x500或父级flex填充)。 - 重复 init 造成内存泄漏
一个 DOM 只init一次;组件卸载时务必dispose()。 - 类型报错(尤其是 
tooltip.formatter)
用unknown+ 运行时守卫从容器对象里安全取name/value/percent。 - SSR 环境(Next.js)
只在客户端渲染时init,例如用动态导入禁用 SSR。 
常用定制方向
- 改主题/颜色:修改 
color数组或全局主题。 - 改布局:
legend.{left, top, orient}与series[0].center。 - 标签与强调:
label、emphasis.label控制中心强调与悬停样式。 - 响应式容器:用 
width: 100%/height: 100%+ 外层自适应布局。 
性能与工程建议
- 避免在每次渲染都 
init,只在首次挂载执行,数据更新仅setOption。 - 大量 resize 事件可做节流/防抖(小型场景直接调用也可)。
 - 异步数据请求要做"已卸载保护"(如上 
cancelled写法)。 
一个最小可用 Option 速查
- 标题 :
title.text/subtext/left/top - 提示框 :
tooltip.trigger/formatter - 图例 :
legend.orient/left/top/data - 系列-饼图 :
series: [{ type: 'pie', radius, center, data }] - 样式 :
itemStyle.borderRadius/borderColor/borderWidth - 动画 :
animation/animationDuration/animationEasing 
如果你把以上要点串起来,就能得到一个生命周期完整、类型安全、可扩展的 ECharts-React 图表组件。基于此模板,你可以快速拓展到轮播图、折线图、柱状图等更多场景,只需更换 series.type 与对应配置即可。
            
            
              tsx
              
              
            
          
          import React, { useEffect, useRef, useState } from "react";
import * as echarts from "echarts";
/**
 * ECharts 演示组件
 *
 * 这个组件演示了如何在 React 中以"正确姿势"接入 ECharts:
 * - 在首次渲染后创建图表实例,并在卸载时销毁,避免内存泄漏
 * - 使用 ref 获取真实 DOM 容器,供 ECharts 渲染
 * - 使用 useEffect 管理图表的创建、更新与清理,保证生命周期完整
 * - 监听窗口大小变化,保持图表自适应
 *
 * 功能特点:
 * 1. 响应式环形饼图展示
 * 2. 自动窗口大小调整(resize)
 * 3. 完整的生命周期管理(初始化/更新/销毁)
 * 4. 基础的性能优化和内存清理
 *
 * 使用场景:
 * - 数据可视化展示
 * - 营销数据分析
 * - 用户行为统计
 *
 * 使用方式:
 * - 直接在页面中引入并渲染 <Demo /> 即可
 * - 如需接入真实数据,只需将下方 chartData 的 mock 数据替换为你的数据,并触发状态更新
 */
// 统一定义图表数据的结构,便于在整个组件中获得类型提示与约束
interface ChartData {
  value: number;
  name: string;
}
const Demo: React.FC = () => {
  // DOM 引用:用于挂载 ECharts 实例。ECharts 需要真实 DOM 节点来渲染图表。
  const chartRef = useRef<HTMLDivElement>(null);
  
  // ECharts 实例引用:保存 init 生成的图表实例,方便在清理阶段 dispose,或在需要时调用实例方法(如 resize)。
  const chartInstanceRef = useRef<echarts.ECharts | null>(null);
  
  // 图表数据状态:可配置的数据源。实际项目中可通过接口获取后 setState 更新。
  const [chartData] = useState<ChartData[]>([
    { value: 100, name: '线下渠道' },
    { value: 200, name: '邮件营销' },
    { value: 300, name: '直接访问' },
    { value: 333, name: '视频广告' },
    { value: 444, name: '搜索引擎' },
  ]);
  /**
   * 初始化并渲染 ECharts 图表
   *
   * 步骤概览:
   * 1) 确认 DOM 容器存在
   * 2) 通过 echarts.init 创建实例,并保存到 ref
   * 3) 准备 Option(图表的所有配置项)
   * 4) setOption 应用配置
   * 5) 绑定 window.resize 事件,保持自适应
   * 6) 返回清理函数:移除事件监听并销毁实例
   *
   * 依赖项:chartData
   * - 当 chartData 变化时,会重新执行 effect,以便刷新图表数据
   */
  useEffect(() => {
    // 确保 DOM 容器已挂载
    if (!chartRef.current) {
      console.warn('Chart container not found');
      return;
    }
    try {
      // 步骤1:初始化 ECharts 实例
      // 注意:同一个 DOM 节点不要重复 init;这里在组件首次挂载时运行。
      const chartInstance = echarts.init(chartRef.current);
      chartInstanceRef.current = chartInstance;
      // 步骤2:配置图表选项(Option 是 ECharts 的核心配置对象)
      const option: echarts.EChartsOption = {
        // 图表标题配置
        title: {
          text: '营销渠道分析',
          subtext: '数据来源:用户行为统计',
          left: 'center',
          top: 20,
          textStyle: {
            fontSize: 18,
            fontWeight: 'bold'
          }
        },
        
        // 提示框配置
        tooltip: {
          trigger: 'item',
          // 使用 unknown + 运行时守卫,兼容 ECharts v6 类型变更并避免 any
          formatter: (params: unknown) => {
            const single = Array.isArray(params) ? params[0] : params;
            const p = (single ?? {}) as Record<string, unknown>;
            const name = typeof p.name === 'string' ? p.name : '';
            const value = typeof p.value === 'number' || typeof p.value === 'string' ? p.value : '';
            const percent = typeof p.percent === 'number' || typeof p.percent === 'string' ? p.percent : '';
            return `
              <div style="padding: 8px;">
                <strong>${name}</strong><br/>
                访问量:${value}<br/>
                占比:${percent}%
              </div>
            `;
          }
        },
        
        // 图例配置
        legend: {
          orient: 'vertical',
          left: 'left',
          top: 'middle',
          // 图例展示每个扇区的名称
          data: chartData.map(item => item.name),
          textStyle: {
            fontSize: 12
          }
        },
        
        // 系列配置
        series: [
          {
            name: '访问来源',
            type: 'pie',
            radius: ['40%', '70%'], // 环形图配置
            center: ['60%', '50%'], // 图表位置
            avoidLabelOverlap: false,
            itemStyle: {
              borderRadius: 10,
              borderColor: '#fff',
              borderWidth: 2
            },
            label: {
              show: false,
              position: 'center'
            },
            emphasis: {
              label: {
                show: true,
                fontSize: 20,
                fontWeight: 'bold'
              }
            },
            labelLine: {
              show: false
            },
            // 最核心的数据入口:每一项代表一个饼图扇区
            data: chartData
          }
        ],
        
        // 颜色配置
        color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'],
        
        // 动画配置
        animation: true,
        animationDuration: 1000,
        animationEasing: 'cubicOut'
      };
      // 步骤3:应用配置到图表
      chartInstance.setOption(option);
      // 步骤4:响应式处理 - 监听窗口大小变化
      const handleResize = () => {
        if (chartInstance && !chartInstance.isDisposed()) {
          chartInstance.resize();
        }
      };
      // 添加窗口大小变化监听
      window.addEventListener('resize', handleResize);
      // 返回清理函数:组件卸载时执行
      return () => {
        // 移除事件监听器
        window.removeEventListener('resize', handleResize);
        
        // 销毁图表实例,释放内存
        if (chartInstance && !chartInstance.isDisposed()) {
          chartInstance.dispose();
        }
        
        // 清空引用
        chartInstanceRef.current = null;
      };
    } catch (error) {
      console.error('Failed to initialize chart:', error);
    }
  }, [chartData]); // 依赖项:当数据变化时重新渲染
  return (
    <div style={{ 
      width: '100%', 
      height: '100vh', 
      display: 'flex', 
      alignItems: 'center', 
      justifyContent: 'center',
      backgroundColor: '#f5f5f5'
    }}>
      {/*
        图表容器:
        - ECharts 会占满容器宽高进行渲染
        - 通过 ref 传递 DOM 给 echarts.init
      */}
      <div 
        ref={chartRef} 
        style={{
          width: '800px',
          height: '500px',
          backgroundColor: '#fff',
          borderRadius: '8px',
          boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
          padding: '20px'
        }}
      />
    </div>
  );
};
export default Demo;
        