
Refine 实战基于 useList 与 AutoComplete 构建跨资源全局搜索 Header【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine本篇指南基于 Refine 仓库中advanced-tutorials/search文档及配套示例工程讲解如何在管理后台顶栏中实现一个跨资源posts、categories的全局搜索框从纯 UI 的Header组件搭建到用useList数据 Hook 按输入值过滤拉取记录、把结果渲染为AutoComplete分组下拉项最后深入useList的源码实现说明filters、enabled: false、refetch在整条调用链中的真实作用。读完你可以直接在 Refine 项目中复制一套可运行的搜索方案并理解其底层数据流。一、方案总览示例工程位于 examples/search其技术栈为refinedev/core提供useList、Link等核心 Hook 与组件refinedev/antd提供ThemedLayout布局、ErrorComponent、通知 Provider 等refinedev/simple-restREST 风格dataProviderantd的AutoCompleteInput搜索输入 UIlodash的debounce输入防抖。数据来源是 Refine 官方 mock APIexamples/search/src/App.tsx 中的const API_URL https://api.fake-rest.refine.dev搜索依赖该 API 对title等字段的contains模糊查询支持。整体思路分三步递进先搭一个只有 UI 的Header通过布局挂载到应用顶栏引入useList根据输入值对posts资源按contains过滤拉取渲染为下拉分组复制同样的useList调用到categories资源实现一个输入框同时搜索多个资源。二、第一步创建纯 UI 的 Header 组件原始文档的第一步是只创建界面、不接任何搜索逻辑。使用 Ant Design 的AutoComplete包裹一个大号Input注意两个关键属性filterOption{false}告诉 Antd 关闭前端本地过滤因为选项数据完全由后端返回本地再过滤反而会漏掉或重复style{{ width: 100%, maxWidth: 550px }}限制搜索框最大宽度使其在宽屏下不撑满顶栏。文档中的原始写法v3 时代的pankod/refine-antd包名import { AntdLayout, AutoComplete, Input, Icons } from pankod/refine-antd; const { SearchOutlined } Icons; export const Header: React.FC () { return ( AntdLayout.Header style{{ padding: 0px 24px, backgroundColor: #FFF, }} AutoComplete style{{ width: 100%, maxWidth: 550px }} filterOption{false} Input sizelarge placeholderSearch posts or categories suffix{SearchOutlined /} / /AutoComplete /AntdLayout.Header ); };对应仓库中当前示例工程的实现见 examples/search/src/components/header.tsx新版额外给顶栏加了position: sticky, top: 0, zIndex: 1让搜索框在页面滚动时保持在视口顶部。三、把 Header 挂载到布局文档提醒别忘了把Header传给Refinev3 写法是Refine ... Header{Header} /。在当前仓库示例中由于 v4 的路由/布局模型已变化Header 改为传给ThemedLayout同时资源路由在Refine的resources属性中显式声明examples/search/src/App.tsxRefine dataProvider{dataProvider(API_URL)} routerProvider{routerProvider} resources{[ { name: posts, list: /posts, show: /posts/show/:id, create: /posts/create, edit: /posts/edit/:id, }, { name: categories, list: /categories, show: /categories/show/:id, create: /categories/create, edit: /categories/edit/:id, }, ]} notificationProvider{useNotificationProvider} options{{ warnWhenUnsavedChanges: true, syncWithLocation: true, }} Routes Route element{ ThemedLayout Header{Header} Outlet / /ThemedLayout } ... /Route /Routes /RefineThemedLayout会把这个Header渲染在侧栏上方、内容区之上形成顶栏。四、定义类型下拉选项与资源记录在写搜索逻辑前先为AutoComplete的options属性和两种资源记录建立接口examples/search/src/interfaces/index.d.tsexport interface IPost { id: number; title: string; content: string; status: published | draft | rejected; category: { id: number }; } export interface ICategory { id: number; title: string; } export interface IOptionGroup { value: string; label: string | React.ReactNode; } export interface IOptions { label: string | React.ReactNode; options: IOptionGroup[]; }其中IOptionGroup对应下拉中的单条结果value是选中值label可以是任意 React 节点因此可以塞进链接IOptions对应一个带标题的分组如 “Posts”、“Categories”。仓库示例的IPost比文档原版多了content、status、category字段与 mock API 的实际返回一致。五、第二步用 useList 按搜索值拉取 posts搜索的核心是用useList数据 Hook 携带“随输入值变化”的过滤条件去请求列表接口。文档给出的 v3 写法pankod/refine-coreconst [value, setValue] useStatestring(); const [options, setOptions] useStateIOptions[]([]); const { refetch: refetchPosts } useListIPost({ resource: posts, config: { filters: [{ field: title, operator: contains, value }], }, queryOptions: { enabled: false, onSuccess: (data) { const postOptionGroup data.data.map((item) renderItem(item.title, posts, item.id), ); if (postOptionGroup.length 0) { setOptions([ { label: renderTitle(Posts), options: postOptionGroup, }, ]); } }, }, }); useEffect(() { setOptions([]); refetchPosts(); }, [value]);关键机制逐条说明filters随value重建filters: [{ field: title, operator: contains, value }]直接引用了 statevalue。每次value变化、组件重渲染时useList感知到新的filters查询键随之更新。enabled: false禁止自动请求搜索框挂载时value为空字符串若自动执行会发出一个“无条件”的请求。把enabled设为false后查询处于“已订阅但不激活”状态只等你显式调用refetch。useEffect驱动重取value变化时先setOptions([])清空旧结果再refetchPosts()用最新 filters 重新拉取。onSuccess中组装分组把返回的每条记录经renderItem映射为{ value, label }非空时才追加一个 “Posts” 分组避免展示空标题。renderTitle与renderItem两个渲染辅助函数负责下拉项的样式与跳转examples/search/src/components/header.tsxconst renderTitle (title: string) { return ( Text strong style{{ fontSize: 16px }} {title} /Text ); }; const renderItem (title: string, resource: string, id: number) { return { value: title, label: ( Link to{/${resource}/show/${id}} Text{title}/Text /Link ), }; };注意renderItem的label是一个指向/{resource}/show/{id}的Link——搜索结果点击后直接导航到该资源的详情页这是搜索组件和路由系统打通的关键。六、第三步一个输入框搜索多个资源文档的最后一步是再声明一个针对categories资源的useList并把setOptions从“覆盖”改为“追加”让两个资源的结果以分组形式并存const { refetch: refetchCategories } useListICategory({ resource: categories, config: { filters: [{ field: q, operator: contains, value }], }, queryOptions: { enabled: false, onSuccess: (data) { const categoryOptionGroup data.data.map((item) renderItem(item.title, categories, item.id), ); if (categoryOptionGroup.length 0) { setOptions((prevOptions) [ ...prevOptions, { label: renderTitle(Categories), options: categoryOptionGroup, }, ]); } }, }, }); useEffect(() { setOptions([]); refetchPosts(); refetchCategories(); }, [value]);这里有一个容易踩坑的细节两个useList的onSuccess是异步先后触发的。如果沿用第二节的setOptions([...])直接覆盖写法后完成的请求会把先完成的分组冲掉。因此多资源版本必须用函数式更新setOptions((prevOptions) [...prevOptions, newGroup])保证两个分组都能累加进同一个options数组。文档同时提示把同样的实现套用到你的其他资源即可用一个输入框搜索任意多个资源。仓库示例工程的等价新写法当前 examples/search/src/components/header.tsx 用 v4/v5 的 API 完成了同一逻辑有三处值得借鉴的改进filters提升为顶层属性useList的filters不再包在config里useEffect观察result而非onSuccess回调useEffect(() {...}, [postsData])监听useList返回的result逻辑更线性onSearch加 500ms 防抖onSearch{debounce((value: string) setValue(value), 500)}配合lodash避免每次击键都打两个请求。const { result: postsData, query: { refetch: refetchPosts }, } useListIPost({ resource: posts, queryOptions: { enabled: false }, filters: [{ field: title, operator: contains, value }], }); const { result: categoriesData, query: { refetch: refetchCategories }, } useListICategory({ resource: categories, queryOptions: { enabled: false }, filters: [{ field: title, operator: contains, value }], }); useEffect(() { if (postsData) { const postOptionGroup postsData.data.map((item) renderItem(item.title, posts, item.id), ); if (postOptionGroup.length 0) { setOptions((prevOptions) [ ...prevOptions, { label: renderTitle(Posts), options: postOptionGroup }, ]); } } }, [postsData]);七、源码视角filters 与 enabled 在 useList 内部如何生效上述行为不是组件魔法可以在 packages/core/src/hooks/data/useList.ts 中逐条印证filters直通 dataProvideruseList解构出filters源码中记为prefferedFilters最终传给Refine注入的dataProvider.getList由 dataProvider如simple-rest将其序列化为 URL 查询参数从源码结构看filters还同时参与 TanStack Query 的查询键构建因此value变化即视为“新查询”触发缓存 key 变更与失效重取。enabled: false的判定源码中const isEnabled queryOptions?.enabled undefined || queryOptions?.enabled true;useList.ts 第 178-179 行。isEnabled控制useResourceSubscription实时订阅等依赖激活状态的能力查询本身的激活交由 TanStack Query 的useQuery选项透传enabled: false时不发出首次请求。refetch的来源返回值中的query就是 TanStack Query 的QueryObserverResultquery.refetch()用“当前查询键对应的最新参数”立即执行一次请求——这正是useEffect里refetchPosts()能用上最新filters的原因。filters 的类型契约filters每一项必须符合CrudFilter类型packages/core/src/contexts/data/types.ts。其中LogicalFilter形如{ field: string; operator: CrudOperators; value: any }而CrudOperators是一组标准操作符操作符含义contains包含不区分大小写containss包含区分大小写ncontains/ncontainss不包含startswith/endswith及s后缀变体前缀 / 后缀匹配eq/ne/gt/lt/in/nin等相等、比较、集合类or/and逻辑组合ConditionalFilter搜索场景选contains不区分大小写的包含最贴合用户直觉若你的后端资源字段名与文档不同如 categories 用title而非q只需替换field这也是文档版与仓库示例版在 categories 过滤字段上的差异所在——以你的 mock API 实际支持的查询字段为准。八、运行示例工程查看与运行当前仓库中的完整示例examples/search/package.json 要求 Node20构建/开发脚本为refine build/refine devcd examples/search pnpm install pnpm dev # 启动开发服务器默认使用 Vite pnpm build # 生产构建tsc refine build启动后打开应用顶栏即出现 “Search posts or categories” 搜索框输入关键词约 500ms 后下拉中按 “Posts”“Categories” 两个分组展示匹配结果点击任一项跳转到对应资源的show/:id详情页。九、可落地的扩展点更多资源按第六节模式为任意资源增加一个useList 一个useEffect分组即可性能务必保留debounce仓库示例为 500ms与filterOption{false}前者减少击键触发的请求后者避免前端重复过滤空输入处理useEffect依赖[value]空字符串同样会触发两个请求可在 effect 内先判断if (!value) return;或用pagination/filters的enabled语义按需跳过结果高亮renderItem的label接受任意 React 节点可自行实现关键词mark高亮多 dataProvider 场景useList支持dataProviderName参数见 useList.ts 的类型定义不同资源走不同后端时可在各 Hook 上分别指定。整套方案没有引入任何额外服务或索引库完全基于 Refine 的useListCrudFilter与 Antd 的AutoComplete组合而成是中小型管理后台实现全局搜索时成本最低、可读性最好的路径。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考