Ariakit Combobox 集成过滤实战:用 useDeferredValue + match-sorter 构建受控搜索组件
UI组件前端【免费下载链接】ariakitToolkit with accessible components, styles, and examples for your next web app项目地址https://gitcode.com/gh_mirrors/ar/ariakit点击查看免费下载本文基于 Ariakit 仓库中的 combobox-filtering-integrated 示例 展开讲解如何通过一个抽象化的Combobox封装结合 React 并发特性React.useDeferredValue与match-sorter库实现输入即过滤的下拉搜索组件。读完本文你将掌握如何把value/onChange受控接口映射到useComboboxStore的inputValue/setInputValue如何用useStoreState高效读取 store 状态以及如何通过 React Context 让ComboboxItem按搜索结果动态挂载/卸载。示例总览官方示例的定位是在 Combobox 组件中通过一个抽象化实现React.useDeferredValue对选项进行过滤从而得到一个更简洁的高层 API。与手动把过滤结果数组传给组件的传统写法不同本示例中使用者只需要把全部条目渲染为ComboboxItem过滤、匹配、空状态全部由封装组件内部处理。最终的使用方式非常干净见 示例入口文件import { Combobox, ComboboxEmpty, ComboboxItem } from ./combobox.tsx; export default function Example() { return ( Combobox autoSelect placeholderSearch food ComboboxEmptyNo results found/ComboboxEmpty ComboboxItem valueApple / ComboboxItem valueBacon / ComboboxItem valueBanana / ComboboxItem valueBroccoli / ComboboxItem valueBurger / {/* ... 共 38 个食品条目 */} ComboboxItem valueYogurt / /Combobox ); }注意几个细节autoSelect打开时自动聚焦首个条目提升键盘操作体验条目是静态写死在 JSX 里的没有任何list.map(...)这是集成过滤写法的核心卖点ComboboxEmpty用于在没有匹配项时显示空状态文案。受控接口把value/onChange映射到useComboboxStore抽象组件的完整实现位于 combobox.tsx。它的 props 定义刻意模仿了 HTML 输入框的受控约定export interface ComboboxProps extends OmitAriakit.ComboboxProps, onChange { value?: Ariakit.ComboboxProviderProps[inputValue]; onChange?: Ariakit.ComboboxProviderProps[setInputValue]; children?: React.ReactNode; }这里value对应 store 的inputValue状态onChange对应setInputValue状态 setter。封装时直接把这两个自定义 props 透传给底层 store 钩子export const Combobox React.forwardRefHTMLInputElement, ComboboxProps( function Combobox({ value, onChange, children, ...props }, ref) { const [list, setList] React.useStatestring[]([]); const combobox Ariakit.useComboboxStore({ inputValue: value, // 受控外部传入当前搜索值 setInputValue: onChange, // 状态变更时回调外部 onChange }); const searchValue Ariakit.useStoreState(combobox, inputValue); // ... }, );这正是 Ariakit Component stores 指南 中描述的受控状态模式把不带default/set前缀的精确状态属性传给 storestore 就不会在内部维护该状态而是只调用你提供的 setter。由此得到两个好处组件可以像原生受控输入框一样被父组件驱动Combobox value{v} onChange{setV} /也可以不传value而以非受控方式使用——store 内部会自行维护inputValue过滤逻辑需要的当前搜索值通过useStoreState读取无论受控与否都能拿到一致的当前值const searchValue Ariakit.useStoreState(combobox, inputValue);从源码结构看useStoreState是 Ariakit 提供的通用状态订阅钩子实现位于 ariakit-react-store 包支持整包订阅 / 指定键订阅 / selector 订阅三种模式只订阅inputValue这一个键意味着组件仅在搜索值变化时才因该值重渲染属于指南中推荐的Watching a specific state property用法。用useDeferredValuematch-sorter计算匹配结果拿到searchValue后示例并没有直接用它做过滤而是先经过一层延迟值// 延迟值可以避免输入时的卡顿源码注释原文Use deferred value to avoid lag when typing const deferredValue React.useDeferredValue(searchValue); // matchSorter 每次渲染都跑可能开销较大因此做记忆化 const matches React.useMemo(() { return matchSorter(list, deferredValue); }, [list, deferredValue]);这里的分工非常清晰React.useDeferredValue把过滤计算这类低优先级更新从高频的按键输入中解耦出来。用户输入时输入框本身的更新高优先级不会被阻塞过滤结果可以在稍后的低优先级更新中追上来从而保持输入流畅matchSorter(list, deferredValue)对已注册的条目列表做模糊匹配match-sorter由示例源码直接import { matchSorter } from match-sorter引入useMemo只有list或deferredValue变化时才重新计算避免每次无关渲染都执行匹配。计算出的matches匹配到的字符串数组连同setList一起打包成 React Context 下发const contextValue React.useMemo(() ({ matches, setList }), [matches]);Context 的定义与类型const ComboboxContext React.createContext{ matches?: string[]; setList?: React.DispatchReact.SetStateActionstring[]; }({});动态渲染ComboboxItem挂载即注册失配即卸载ComboboxItem的抽象是整个示例最核心的部分源码见 combobox.tsxexport const ComboboxItem React.forwardRefHTMLDivElement, ComboboxItemProps( function ComboboxItem({ value, ...props }, ref) { const { matches, setList } React.useContext(ComboboxContext); // 挂载时把条目加入 list卸载时移除源码注释原文 React.useLayoutEffect(() { if (!setList) return; if (value null) return; setList((list) [...list, value]); return () { setList((list) list.filter((v) v ! value)); }; }, [setList, value]); const match value ! null matches?.includes(value); // 条目不在匹配列表中就不渲染 if (!match) return null; return ( Ariakit.ComboboxItem ref{ref} focusOnHover blurOnHoverEnd{false} {...props} value{value} className{clsx(combobox-item, props.className)} / ); }, );拆开看有三个关键机制条目自注册self-registration每个ComboboxItem通过useLayoutEffect在挂载时把自己的value追加进父组件的list状态在卸载清理函数中移除。list因此始终与当前渲染中的条目保持同步——父组件完全不需要手动维护选项数组。选择useLayoutEffect而非useEffect是为了在浏览器绘制前完成注册避免首帧匹配列表缺项条件渲染即过滤matches.includes(value)为 false 时直接return null。也就是说过滤效果是靠条目组件自行决定渲染与否达成的DOM 中永远只有命中的条目。这与非集成版示例 combobox-filtering 的写法形成对照——后者是显式地matches.map((value) ComboboxItem key{value} value{value} /)由父级计算并传入结果数组交互行为配置条目统一带上focusOnHover悬停即聚焦与blurOnHoverEnd{false}悬停结束时不主动移除焦点保证鼠标与键盘焦点行为一致。一个重要的顺序限制需要特别理解这种条件渲染式的过滤无法改变条目的显示顺序——被过滤后的条目始终保持原始列表中的顺序。原因从源码即可看出ComboboxItem只判断matches.includes(value)成员关系从不读取该条目在matches中的索引或排序位置而 DOM 顺序由 JSX 中条目的书写顺序决定。如果你希望过滤后保持原始顺序比如菜单按固定分组/权限排列这个方案非常合适如果你依赖matchSorter的相关性排序匹配度高的排前面则此写法不满足需求应改用非集成写法直接对matches数组做map渲染让匹配结果的顺序驱动渲染顺序。ComboboxEmpty基于匹配计数的空状态空状态组件同样消费 Context逻辑极简combobox.tsxexport const ComboboxEmpty React.forwardRefHTMLDivElement, ComboboxEmptyProps( function ComboboxEmpty(props, ref) { const { matches } React.useContext(ComboboxContext); // 只要有匹配项就完全不渲染 if (matches?.length) return null; return ( Ariakit.Role ref{ref} {...props} className{clsx(no-results, props.className)} / ); }, );它渲染的是Ariakit.Role一个不带内置行为、只用于承载样式与结构语义的基座组件在matches为空数组时显示 children示例中为 No results found。样式由 style.css 提供该文件通过import复用 combobox-filtering 的样式其中.no-results类附加了间距与内边距。弹出层配置ComboboxPopover的实用参数封装组件把输入框与弹出层一并返回ComboboxPopover上配置了几个值得注意的参数combobox.tsx参数作用store{combobox}将输入框与弹出层接入同一个 store建立类似aria-controls的部件关联portal将弹出层渲染到 portal 中避免被父级overflow裁剪sameWidth弹出层宽度与输入框保持一致gutter{8}弹出层与锚点元素之间保留 8px 间距unmountOnHide隐藏时卸载弹出层子树注意条目卸载后useLayoutEffect清理函数会把值移出list再次打开时会重新注册children被包裹在ComboboxContext.Provider内再交给弹出层这保证了ComboboxItem/ComboboxEmpty即使不显式接收storeprop也能通过 Context 拿到matches与setList。与非集成版combobox-filtering的对照仓库中同系列的 combobox-filtering 示例 是显式过滤写法两者构成一组递进关系维度combobox-filtering显式combobox-filtering-integrated集成状态控制ComboboxProvider的setValue回调内部用React.startTransition包裹setSearchValueuseComboboxStore的inputValue/setInputValue对外暴露受控value/onChange防卡顿手段React.startTransition把过滤状态更新标记为可中断的过渡更新React.useDeferredValue延迟搜索值再配合useMemo记忆化匹配结果条目渲染matches.map((value) ComboboxItem ... /)顺序由匹配结果决定可利用排序静态写死全部ComboboxItem value... /靠条件渲染过滤顺序固定为原始顺序使用者心智需要维护list、matches与渲染映射只需声明条目过滤逻辑完全内聚在封装组件中两种写法的过滤算法都可以自由替换官方文档提到除match-sorter外还可以选用采用模糊搜索算法的fast-fuzzy等库Combobox组件本身对过滤策略是无关的agnostic——它只关心你通过ComboboxItem渲染出的条目。小结与相关示例本文覆盖的 combobox-filtering-integrated 示例提供了三层可复用的经验API 抽象层用Omit..., onChange 自定义value/onChangeprops把内部 store 细节藏起来对外保持熟悉的受控组件契约并发过滤层useDeferredValueuseMemo保证高频输入下 UI 不卡顿条目自治层ComboboxItem自注册 条件渲染配合 Context 中的matches实现声明式过滤与空状态。需要留意顺序限制过滤不改变条目顺序这一设计权衡按需选择本示例或显式映射写法。以下相关示例可继续深入 Combobox 的其他能力均为仓库内路径combobox-filtering显式过滤 startTransitioncombobox-animated带动画的下拉combobox-cancel可取消的过滤输入combobox-multiple多选标签式 Comboboxcombobox-tabs与 Tabs 组合的导航型 Comboboxdialog-combobox-tab-command-menuDialog Combobox CommandMenu 复合弹窗menu-nested-combobox嵌套菜单中的 Combobox更多设计原理可参考仓库内文档Combobox 组件说明 与 Component stores 指南。赞分享UI组件前端【免费下载链接】ariakitToolkit with accessible components, styles, and examples for your next web app项目地址https://gitcode.com/gh_mirrors/ar/ariakit点击查看免费下载相关推荐Sapphire框架49最佳实践10个提升Discord机器人性能的技巧Sapphire框架49最佳实践10个提升Discord机器人性能的技巧 Sapphire框架49是基于discord.js构建的高级Discord机器人框架Cycle.js响应式搜索过滤组件构建可复用的高级搜索组件Cycle.js响应式搜索过滤组件构建可复用的高级搜索组件 你是否在开发搜索功能时遇到过用户输入卡顿、下拉菜单闪烁或结果更新不及时的问题Cycle.js的响前端Web框架coss Combobox 实战指南在 Kaneo 中构建可搜索选择组件coss Combobox 实战指南在 Kaneo 中构建可搜索选择组件 导读 Combobox组合框是可搜索选择的 UI 原语它把文本输入与列表选择合企业应用后端前端上一篇opsu!游戏机制解析圆圈、滑条和转盘的操作技巧下一篇MaterialFiles深度剖析Android平台最强Material Design文件管理器横空出世创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考