RemoteSelect
remote-select远程搜索选择器 · 防抖搜索 + AbortSignal 取消 + 滚到底分页 + resolveValue 初值回显,支持多选 chips
用法
基础用法
输入即防抖搜索(300ms),滚到底自动追加下一页,本地不做二次过滤。
tsx
<RemoteSelect
valueKey="store_id"
labelKey="store_name"
placeholder="搜索门店…"
fetcher={async (query, { page, pageSize, signal }) => {
const res = await fetch(`/api/stores?q=${query}&page=${page}&size=${pageSize}`, { signal })
const json = await res.json()
return { options: json.list, total: json.total }
}}
/>初值回显(编辑表单必配)
value 已有但不在首屏列表里时,用 resolveValue 按 id 批量解一次 label;不配就只能显示裸 id。
tsx
<RemoteSelect
valueKey="store_id"
labelKey="store_name"
defaultValue="1055"
fetcher={fetchStores}
// 与 fetcher 分开:它按 id 取详情,不参与搜索/分页
resolveValue={async (ids) => {
const res = await fetch(`/api/stores/batch?ids=${ids.join(",")}`)
return (await res.json()).list
}}
/>多选
chip 严格按 value 顺序渲染;已选但未加载的项同样能回显文案。
1058
1002
tsx
<RemoteSelect
multiple
valueKey="store_id"
labelKey="store_name"
defaultValue={["1058", "1002"]}
fetcher={fetchStores}
resolveValue={resolveStores}
/>自定义选项行
renderOption 可拿到 raw 原始行,渲染副标题 / 编号 / 标签等。
tsx
<RemoteSelect
valueKey="store_id"
labelKey="store_name"
pageSize={8}
fetcher={fetchStores}
renderOption={(option) => (
<span className="flex min-w-0 flex-1 items-center gap-2">
<span className="truncate text-foreground">{option.label}</span>
<span className="ml-auto shrink-0 text-xs text-muted">#{String(option.raw.store_id)}</span>
</span>
)}
/>尺寸 / 禁用 / 无效态
size 控制字段高度;disabled 整体置灰;invalid 标红边框。
tsx
<>
<RemoteSelect size="sm" fetcher={fetchStores} placeholder="搜索门店…" />
<RemoteSelect disabled fetcher={fetchStores} placeholder="搜索门店…" />
<RemoteSelect invalid fetcher={fetchStores} placeholder="搜索门店…" />
</>何时用
选项来自后端接口、数据量大到不可能一次性拉全(门店、会员、商品、员工…)时用:输入防抖搜索、滚动加载下一页、编辑表单打开即回显已选项的中文名。
选项固定且已在前端就用 Select;选项在前端数组里只是需要搜索用 Combobox(本组件即基于它,只是把过滤权从本地交给了服务端);国家/地区这类内置数据用 CountrySelect。
导入
ts
import { RemoteSelect } from "@hulianui/ui"Props
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| fetcher * | (query, { page, pageSize, signal }) => Promise<{ options, total? }> | — | 远程搜索数据源。options 是后端原始行数组;total 给了就用它判断还有没有下一页 |
| resolveValue | (values: string[]) => Promise<Row[]> | — | 初值回显解析器:按 value 批量解 label。编辑表单必配,见下方「禁忌 / 坑」 |
| labelKey | string | "name" | 从原始行取显示文案的字段名 |
| valueKey | string | "id" | 从原始行取值的字段名 |
| debounce | number | 300 | 输入防抖毫秒数 |
| pageSize | number | 10 | 每页条数,透传给 fetcher |
| multiple | boolean | false | 多选(chips 形态)。开启后 value/onChange 变数组 |
| value | string|number|null(多选为数组) | — | 受控值。数组顺序即 chip 渲染顺序 |
| defaultValue | 同上 | — | 非受控初值 |
| placeholder | string | "请选择" | 字段占位 |
| emptyMessage | ReactNode | "无匹配数据" | 空态文案 |
| loadingMessage | ReactNode | "加载中…" | 加载态文案 |
| size | "sm"|"md"|"lg" | "md" | 尺寸 |
| clearable | boolean | true | 单选时渲染清除按钮(多选靠 chip 上的 × 逐个删) |
| disabled | boolean | false | 禁用 |
| invalid | boolean | false | 独立使用(非 Field 内)时手动置无效态皮肤 |
| defaultOpen | boolean | false | 非受控初始展开(调试 / 文档演示用) |
| renderOption | (option) => ReactNode | — | 自定义选项行(option.raw 是后端原始行) |
| className | string | — | 字段(输入框 / chips 外壳)类名 |
| popupClassName | string | — | 浮层类名 |
Events
| 事件 | 类型 | 说明 |
|---|---|---|
| onChange | 单选 (value: string|null, option: Option|null) => void<br>多选 (value: string[], options: Option[]) => void | 选中变化。第二参给出完整选项(含 raw 原始行),与 value 同序 |
禁忌 / 坑
- `resolveValue` 不是可选装饰,编辑表单必配:打开编辑表单时
value已有,但它常常不在首屏那一页里(在第 7 页、或被当前搜索词过滤掉)。只有resolveValue能把它的 label 解出来,缺了就只会显示裸 id。它与fetcher是两个不同的后端语义(一个按关键词分页搜,一个按主键批量取),别合并成一个函数。 - `fetcher` 必须把 `signal` 透传给 fetch/axios:不传也能跑(组件用请求序号丢弃过期响应),但旧请求会一直占着连接,快速输入时可能连开十几条。
- 多选 chip 只能按 `value` 顺序渲染:底层
ComboboxChipRemove按 chip 在容器内的渲染序绑定selectedValue[index],乱序渲染会删错项。组件内部已按value顺序渲染,自定义时别打乱。 - 本地不做二次过滤(底层
filter={null}):搜索结果完全由服务端决定,fetcher忽略query就等于没有搜索。 - 关闭浮层即结束一次搜索会话:关键词清空、下次打开重新拉第一页(与 el-select 的 remote 行为一致),因此不要在
fetcher里做「同参数缓存穿透」以外的副作用。 total不给也能分页:此时按「本页返回条数 ≥ pageSize 即可能还有下一页」推断,最后会多打一次空请求;能给就给。
相关
Combobox · Select · CountrySelect · Cascader · ProTable
Playground
<RemoteSelect
valueKey="store_id"
labelKey="store_name"
size="md"
placeholder="搜索门店…"
clearable={true}
fetcher={fetchStores}
resolveValue={resolveStores}
/>