Filterable Data Page
Overview
可筛选、分页列表页的标准实现:URL 为唯一数据源(nuqs),数据加载用 TanStack Query,Container/Filter/Display 三层分离。避免「state + URL 双源」导致的翻页重复请求。
When to Use
适用: 多筛选条件、分页、分享链接/刷新保持、切换筛选需保留旧数据防闪烁。
不适用: 无筛选、筛选不需进 URL、非 React。
Quick Start
- •URL 状态:用 nuqs 的
useQueryStates管理page与所有筛选参数;翻页/筛选只调 nuqs setter,不维护currentPage/filters的 useState。 - •数据加载:queryKey 包含从 URL 解析出的全部参数(如
[page, ...filters]);数据请求只依赖这些值,一次 URL 变化对应一次请求。 - •分层:Container 负责 nuqs + Query + 分发状态;Filter 只接收当前值与 onChange;Display 只接收 data/分页信息。
- •筛选变更:任意筛选变化时用
setUrlState({ page: 1, ...newFilters })一次性更新并重置分页。 - •加载状态:首次用
isLoading(Skeleton),后续用isFetching;配置placeholderData: keepPreviousData防闪烁。
反模式:双源导致翻页重复请求
现象: 翻页时同一 ?page=N 请求 2~3 次。
原因: 页码/筛选同时被本地 state 和 URL 驱动(如 setCurrentPage + router.push,再在 useEffect 里从 searchParams 解析并 setState),多次 setState 触发多次依赖它们的 effect。
正确: 以 URL 为唯一数据源,用 nuqs 管理 page 与筛选;数据加载只依赖 nuqs 解析出的值。
Checklist
实现或审查可筛选/分页页时:
- • 使用 nuqs
useQueryStates管理 page 与筛选,无单独 useState 存 page/filters - • 翻页/筛选只更新 URL(nuqs setter),不先 setCurrentPage 再 router.push
- • queryKey / 数据请求依赖仅来自 URL 解析结果
- • 筛选变更时重置分页:
setUrlState({ page: 1, ...changes }) - • 配置
placeholderData: keepPreviousData;区分 isLoading / isFetching - • Filter 只接收回调;Display 只接收 props;Container 负责 URL 与数据
Common Mistakes
| 错误做法 | 正确做法 |
|---|---|
| 筛选变更时忘记重置分页 | 始终 setUrlState({ page: 1, ...changes }) |
用 isLoading 控制所有加载状态 | 首次 isLoading,后续 isFetching |
Filter 内部调用 setUrlState | Filter 只接收回调,Container 负责 URL |
每个筛选条件单独 useState | 统一 useQueryStates 管理 URL 参数 |
忘记 placeholderData | 配置 keepPreviousData 防闪烁 |
搜索框用 history: 'push' | 搜索框用 history: 'replace' |
| 级联筛选分两次 setState | 一次性:{ leader: x, event: null, page: 1 } |
| Display 内部获取数据 | Display 只接 props,Container 负责数据 |
| useState 存 page/filters + 翻页时 setCurrentPage + router.push + useEffect 从 searchParams 解析 setState | 双源导致多次请求;改用 nuqs 或翻页只 updateUrl 不 setCurrentPage |
详细说明
Pattern 1(nuqs 解析器、useQueryStates、重置分页、级联筛选)、Pattern 2(queryKey、keepPreviousData、isLoading/isFetching、渲染优先级)、Pattern 3(三层架构与数据流)见 reference.md。