首页
/ Ant Design Cascader 动态加载(loadData 懒加载)组件实战指南

Ant Design Cascader 动态加载(loadData 懒加载)组件实战指南

2026-09-06 18:25:52作者:韦蓉瑛

本文以 ant-design 仓库中的 Cascader 懒加载示例文档 及其配套 示例源码 lazy.tsx 为主体,结合 Cascader API 文档 与组件入口源码,系统讲解如何用 loadData 实现选项的按需动态加载,覆盖数据模型设计、isLeaf 叶子标记、changeOnSelect 配合技巧、与 showSearch 互斥的底层原因,以及接入真实后端接口时的工程化建议。读完即可上手实现"省市区级联 + 远程拉取子级"这类典型场景。

示例要解决什么问题

在使用级联选择器(Cascader)时,常规做法是把整棵 options 树一次性交给组件。但当数据量庞大(如全国行政区划、组织架构、商品分类树)时,预先加载全部节点既慢又浪费流量。更好的做法是"用到再加载":用户展开某一层级时,才异步请求它的直接子节点并注入选项树。

仓库中的懒加载 demo 正是这一能力的官方最小演示。它的示例说明只有两句话:

  • 使用 loadData 实现动态加载选项(Load options lazily with loadData)。
  • 注意:loadDatashowSearch 无法一起使用。

下面我们先把完整示例源码逐段拆解,再深入 API 参数与源码层面的机制。

数据模型:用 isLeaf: false 标记"可展开但暂无子级"的节点

懒加载的第一个关键点在于初始 options 中不包含 children。示例数据只有两级"省"级别节点,且每个节点都显式声明了 isLeaf: false

interface Option {
  value?: string | number | null;
  label: React.ReactNode;
  children?: Option[];
  isLeaf?: boolean;
}

const optionLists: Option[] = [
  { value: 'zhejiang', label: 'Zhejiang', isLeaf: false },
  { value: 'jiangsu', label: 'Jiangsu', isLeaf: false },
];

对照 API 文档中 Option 的 TypeScript 定义

interface Option {
  value: string | number;
  label?: React.ReactNode;
  disabled?: boolean;
  children?: Option[];
  // 标记是否为叶子节点,设置了 `loadData` 时有效
  // 设为 `false` 时会强制标记为父节点,即使当前节点没有 children,也会显示展开图标
  isLeaf?: boolean;
}

可以看到组件注释明确说明了两条语义:

  1. isLeaf 仅在设置了 loadData 时有效;
  2. isLeaf 设为 false强制把该节点标记为父节点——即使它此刻没有 children,也会显示"可展开"的箭头图标,从而给 loadData 的触发创造条件。

反过来说:如果不设置 isLeaf: false,一个没有 children 的节点会被组件判定为叶子节点,用户根本看不到展开箭头,也就无法触发动态加载。这是懒加载最常见的一个坑。

loadData 的核心实现:写子级 + 用"新数组引用"刷新

看示例中的核心回调:

const loadData = (selectedOptions: Option[]) => {
  const targetOption = selectedOptions[selectedOptions.length - 1];

  // load options lazily
  setTimeout(() => {
    targetOption.children = [
      { label: `${targetOption.label} Dynamic 1`, value: 'dynamic1' },
      { label: `${targetOption.label} Dynamic 2`, value: 'dynamic2' },
    ];
    setOptions([...options]);
  }, 1000);
};

逐点分析其工作机理:

  • 参数语义loadData 收到的 selectedOptions 是当前已被用户逐级选中的节点路径数组。当用户展开"Zhejiang"时,该数组形如 [{ value: 'zhejiang', label: 'Zhejiang' }];再往下一级时数组会随之变长。因此示例用 selectedOptions[selectedOptions.length - 1] 取到最后一个、也就是刚被展开的那一层父节点lazy.tsx 第 33 行)。
  • 写回子级:拿到 targetOption 后,把异步结果以标准 children 结构写回该节点对象。
  • 为什么必须 setOptions([...options]):由于 targetOption 是原 options 树中某个对象的引用,直接原地改它的 children 不会改变数组引用,React 无法感知状态变化。示例用展开运算符生成一个新数组再 setOptions,触发组件基于新 props 重新渲染,从而让新注入的子级出现在菜单中。
  • 模拟异步:示例用 setTimeout(..., 1000) 模拟 1 秒网络延迟,真实项目应替换为 fetch/axios 请求。

演示环境下 setTimeout 内部直接读取了外层 options,存在闭包读取旧值的隐患;生产中更稳健的写法是在 setOptions 时基于"当前最新值"派生,例如使用函数式更新,避免并发请求时相互覆盖。

组件接线:loadData + changeOnSelect + onChange

示例最终渲染如下:

return (
  <Cascader
    options={options}
    loadData={loadData}
    onChange={onChange}
    changeOnSelect
  />
);

各 prop 的作用与默认值对照 Cascader API 表

参数 说明 类型 默认值
options 可选项数据源 Option[] -
loadData 用于动态加载选项,无法与 showSearch 一起使用 (selectedOptions) => void -
changeOnSelect 单选时生效(multiple 下始终可以),点选每级菜单选项值都会变化 boolean false
onChange 选择完成后的回调 (value, selectedOptions) => void -
loadingIcon 自定义的加载图标 ReactNode -

示例特意开启了 changeOnSelect,因为懒加载通常配合"选中任意一级即生效"的交互(例如只选到"浙江省"这一级就提交),而不是必须一路选到叶子节点。onChange 回调的两个参数中,value 是路径值数组(如 ['zhejiang', 'dynamic1']),selectedOptions 是对应的节点对象路径数组,可用于拿到 label 或额外字段。

加载期间,展开菜单会显示 loading 图标,可通过 loadingIcon 定制;在 index.test.tsx 的 loadingIcon 用例 中可以看到 loadingIcon 既可作为 prop 传入,也可由 ConfigProvidercascader.loadingIcon 全局配置,且 prop 优先级高于 ConfigProvider 配置

从源码看 loadData 的实现层级

ant-design 的 Cascader 是对 @rc-component/cascader 的封装。在组件入口 components/cascader/index.tsx 中:

  • CascaderProps 通过 Omit<RcCascaderProps<...>, ...> 继承了底层组件的全部 props(index.tsx 第 144-147 行),因此 loadData 的类型直接来自底层 rc 组件;
  • 在渲染时,antd 层只解析并接管样式、状态、图标等 UI 相关 props,而把 restProps 一并透传给 RcCascaderindex.tsx 第 477 行{...(restProps as any)}),loadData 即由底层组件消费并驱动"展开时加载"的行为;
  • isLeafchildren 的字段名可通过 fieldNames 自定义(默认 { label: 'label', value: 'value', children: 'children' })。如果后端返回的字段是 { name, id, list } 而非 label/value/children,需要配合 fieldNames={{ label: 'name', value: 'id', children: 'list' }} 使用——注意该映射同样作用于 loadData 写回的子级结构。相关行为在 index.test.tsx 的 fieldNames 用例 中也有覆盖。

这意味着:从仓库源码结构看,动态加载的展开触发、loading 状态管理与节点懒注入判定都沉淀在 rc 层,antd 层负责对外暴露稳定的 loadData 接口与样式封装。示例中的"改 children + 拷贝新数组"正是让 rc 层能在下一次渲染时看到新数据的标准做法。

为什么 loadData 与 showSearch 互斥

示例文档以醒目方式给出了一个约束:

注意:loadDatashowSearch 无法一起使用。(Note: loadData cannot work with showSearch

结合源码可以这样理解其本质:

  • 级联菜单的懒加载是基于"用户逐级展开"这一路径驱动的:展开到哪一层才加载哪一层的 children,未展开过的节点始终没有子级数据;
  • showSearch 的搜索机制需要把整个选项路径收集起来做过滤。在 index.tsx 的 defaultSearchRender 与内置 filter 逻辑中,搜索基于 path(从根到当前节点的完整节点链)逐级匹配;同时 showSearch 对象形式还提供 filtersortlimitrendersearchValueonSearch 等字段(见 showSearch 小节),其默认 limit 为 50,说明它面向的是完整载入的整棵树

换句话说,懒加载树的绝大多数子级数据根本不在内存里,搜索无从比对;若强行开启,要么搜不到未加载的节点,要么为支持搜索而预加载全量数据,反而违背懒加载初衷。因此官方在设计与文档层面直接禁止二者同时使用。需要"远程搜索 + 级联"的需求,应自行在搜索回调中请求后端并维护结果列表,而非依赖 Cascader 内置的 showSearch

从示例到真实接口:一个可直接运行的进阶版本

把示例中的 setTimeout 换成真实请求、并规避闭包引用问题后,一个典型的懒加载实现大致如下(可直接在支持 antd 的项目中运行):

import React, { useState } from 'react';
import type { CascaderProps } from 'antd';
import { Cascader } from 'antd';

interface Option {
  value?: string | number | null;
  label: React.ReactNode;
  children?: Option[];
  isLeaf?: boolean;
}

const fetchChildren = (parent: Option): Promise<Option[]> =>
  // 替换为真实接口:fetch(`/api/regions?pid=${parent.value}`)
  new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { value: `${parent.value}-1`, label: `${parent.label} 子级 1` },
        { value: `${parent.value}-2`, label: `${parent.label} 子级 2`, isLeaf: true },
      ]);
    }, 800);
  });

const App: React.FC = () => {
  const [options, setOptions] = useState<Option[]>([
    { value: 'zhejiang', label: 'Zhejiang', isLeaf: false },
    { value: 'jiangsu', label: 'Jiangsu', isLeaf: false },
  ]);

  const loadData: CascaderProps<Option>['loadData'] = (selectedOptions) => {
    const targetOption = selectedOptions[selectedOptions.length - 1];

    // 触发加载态(组件级 loading 指示由底层实现管理)
    fetchChildren(targetOption)
      .then((children) => {
        // 基于最新 options 派生新树,避免并发请求相互覆盖
        setOptions((prev) => {
          const walk = (list: Option[]): Option[] =>
            list.map((item) => {
              if (item.value === targetOption.value) {
                return { ...item, children };
              }
              return item.children ? { ...item, children: walk(item.children) } : item;
            });
          return walk(prev);
        });
      });
  };

  return (
    <Cascader
      options={options}
      loadData={loadData}
      changeOnSelect
      loadingIcon={<span style={{ fontSize: 12 }}>加载中…</span>}
      placeholder="请选择区域"
    />
  );
};

export default App;

工程化进阶提醒:

  • 错误与竞态处理:请求失败时应提示并保留展开能力,避免"永远转圈";连续快速展开不同父级时,可对过期的异步响应做丢弃判断。
  • 缓存子级:对已加载过的父节点缓存结果,二次展开不再请求;也可结合 loading 相关受控能力在顶层统一管理加载态。
  • 自定义字段:若后端使用非 label/value/children 命名,配合 fieldNames 映射,并把服务端返回的叶子标记(如 hasChildren)翻译为 isLeaf 反向语义(isLeaf = !hasChildren)。
  • 更多级联交互:可继续对照仓库中 changeOnSelect 示例(选择任意层级即触发 onChange)、hover 示例expandTrigger="hover" 悬停展开)与 fields-name 示例(自定义字段名)组合出适合业务的形态。

小结

Cascader 的懒加载能力由"数据标记isLeaf: false)+ 加载回调loadData)+ 引用刷新(拷贝新 options)"三者共同构成:

  1. 初始数据只给父级,父级用 isLeaf: false 声明可展开;
  2. 用户展开时,loadData 收到已选路径,取末位节点异步请求其子级;
  3. 将子级写入节点后以新数组引用 setOptions,组件刷新出下一级菜单;
  4. 记得 loadData 不能与 showSearch 共用,需要搜索时走自定义远程搜索方案。

如需继续深入,可直接阅读本仓库中的源码与用例:懒加载示例 lazy.tsx示例描述 lazy.mdCascader API 文档(中文)组件入口实现 以及 loadingIcon 相关测试

登录后查看全文
热门项目推荐
相关项目推荐