Ant Design Cascader 动态加载(loadData 懒加载)组件实战指南
本文以 ant-design 仓库中的 Cascader 懒加载示例文档 及其配套 示例源码 lazy.tsx 为主体,结合 Cascader API 文档 与组件入口源码,系统讲解如何用
loadData实现选项的按需动态加载,覆盖数据模型设计、isLeaf叶子标记、changeOnSelect配合技巧、与showSearch互斥的底层原因,以及接入真实后端接口时的工程化建议。读完即可上手实现"省市区级联 + 远程拉取子级"这类典型场景。
示例要解决什么问题
在使用级联选择器(Cascader)时,常规做法是把整棵 options 树一次性交给组件。但当数据量庞大(如全国行政区划、组织架构、商品分类树)时,预先加载全部节点既慢又浪费流量。更好的做法是"用到再加载":用户展开某一层级时,才异步请求它的直接子节点并注入选项树。
仓库中的懒加载 demo 正是这一能力的官方最小演示。它的示例说明只有两句话:
- 使用
loadData实现动态加载选项(Load options lazily withloadData)。 - 注意:
loadData与showSearch无法一起使用。
下面我们先把完整示例源码逐段拆解,再深入 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;
}
可以看到组件注释明确说明了两条语义:
isLeaf仅在设置了loadData时有效;- 将
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 传入,也可由 ConfigProvider 的 cascader.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一并透传给RcCascader(index.tsx 第 477 行 的{...(restProps as any)}),loadData即由底层组件消费并驱动"展开时加载"的行为; isLeaf、children的字段名可通过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 互斥
示例文档以醒目方式给出了一个约束:
注意:
loadData与showSearch无法一起使用。(Note:loadDatacannot work withshowSearch)
结合源码可以这样理解其本质:
- 级联菜单的懒加载是基于"用户逐级展开"这一路径驱动的:展开到哪一层才加载哪一层的
children,未展开过的节点始终没有子级数据; - 而
showSearch的搜索机制需要把整个选项路径收集起来做过滤。在 index.tsx 的 defaultSearchRender 与内置 filter 逻辑中,搜索基于path(从根到当前节点的完整节点链)逐级匹配;同时showSearch对象形式还提供filter、sort、limit、render、searchValue、onSearch等字段(见 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)"三者共同构成:
- 初始数据只给父级,父级用
isLeaf: false声明可展开; - 用户展开时,
loadData收到已选路径,取末位节点异步请求其子级; - 将子级写入节点后以新数组引用
setOptions,组件刷新出下一级菜单; - 记得
loadData不能与showSearch共用,需要搜索时走自定义远程搜索方案。
如需继续深入,可直接阅读本仓库中的源码与用例:懒加载示例 lazy.tsx、示例描述 lazy.md、Cascader API 文档(中文)、组件入口实现 以及 loadingIcon 相关测试。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00