筛选与排序组件能不能用,取决于状态往哪放。把多条件筛选、排序字段、当前页码全部同步进 URL,页面刷新不丢、链接可分享、浏览器后退能复原,才是监控面板该有的交互。AI 热点监控工具里,用户常按平台(Twitter / B站)、热度区间、时间窗、关键词组合检索,状态一旦只存在组件内存里,刷新就归零,体验直接崩。下文给出以 URL 为统一真相源的实现方案。
一、筛选排序组件要解决的三件事
组合筛选不是把几个下拉框拼起来。它要同时回答三个问题:当前选了什么、选了之后数据怎么变、用户怎么把当前视图分享给别人。
| 关注点 | 错误做法 | 正确做法 |
|---|---|---|
| 状态存放 | 只存组件 state | 以 URL 查询参数为真相源 |
| 历史栈 | 每次筛选都 push | replaceState 静默更新 |
| 页码联动 | 排序后停留旧页 | 变更排序/筛选即重置到第 1 页 |
把 URL 当真相源后,筛选组件变成 URL 的「视图投影」:读 URL 渲染控件,改控件写回 URL,数据请求盯住 URL。这一层解耦让分享、刷新、后退都自然成立。
二、用 URLSearchParams 序列化筛选状态
避免手写 ?a=1&b=2 拼串,改用 URLSearchParams 统一处理:自动编码、支持多值、方便增删。数组型筛选(如多选平台)用相同 key 多次 append;空值与默认值不写入 URL,保持链接干净。
function buildQuery(state: FilterState): string {
const p = new URLSearchParams();
if (state.platform.length) state.platform.forEach(v => p.append('plat', v));
if (state.sort) p.set('sort', state.sort);
if (state.minHeat) p.set('heat', String(state.minHeat));
if (state.page > 1) p.set('page', String(state.page));
return p.toString();
}
写入地址栏用 replaceState 而非 pushState,避免每选一次条件就往历史栈塞一条记录,否则用户连点后退会卡在筛选状态里出不来:
function syncToUrl(state: FilterState) {
const qs = buildQuery(state);
const url = qs ? `${location.pathname}?${qs}` : location.pathname;
history.replaceState({ filters: state }, '', url);
}
三、初始化时从 URL 还原控件
组件挂载时只解析一次 URL,把结果作为初始 state。用 useMemo 包一层,即使在严格模式下也只跑一次,避免重复请求。
function readParams(): FilterState {
const p = new URLSearchParams(location.search);
return {
platform: p.getAll('plat'),
sort: p.get('sort') ?? '',
minHeat: Number(p.get('heat') ?? 0),
page: parseInt(p.get('page') ?? '1', 10),
};
}
非法参数要静默处理:page 解析出来是 NaN 就回落到 1,sort 不在白名单里就当空值。别把脏 URL 直接喂给后端,否则可能触发异常响应。
四、排序变更必须重置页码
这是常漏的一步。用户在靠后页面把排序从「热度」切到「时间」,后端返回的新结果可能只有两页,但组件还停在 page=9,于是页面空白。
function handleSortChange(next: string) {
setSort(next);
setPage(1); // 关键:排序或筛选变化立即回到首页
syncToUrl({ ...current, sort: next, page: 1 });
}
排序选项本身应抽成配置,便于统一维护与国际化:
export const SORT_OPTIONS = [
{ label: '热度从高到低', value: 'heat-desc' },
{ label: '时间从新到旧', value: 'time-desc' },
{ label: '互动量优先', value: 'engage-desc' },
];
五、输入类筛选要做防抖
关键词输入框每次击键都改 URL、发请求,会刷爆接口。输入类筛选防抖 300ms,开关与下拉类即时更新。监听 popstate 响应浏览器前进后退,优先读 event.state 里的筛选快照,读不到再解析 location.search。
import debounce from 'lodash.debounce';
const onSearch = debounce((term: string) => {
setKeyword(term);
syncToUrl({ ...current, keyword: term, page: 1 });
}, 300);
六、状态流转的推荐结构
把筛选组件拆成「受控视图 + 同步副作用」两段,结构更清晰:
- 定义 FilterState 类型,覆盖平台、排序、热度阈值、关键词、页码;
- 挂载时 readParams 初始化,写回各控件选中态;
- 任意控件变更 → 更新内存 state → 重置页码 → syncToUrl;
- 监听 popstate,用快照或解析 URL 还原 state;
- 数据层用 state 作为查询键,state 变即重新拉取。
这套结构让筛选、排序、分页三者通过同一个 URL 串联,刷新不丢、分享即得、后退可追,是监控工具检索交互的稳妥基线。
常见问题(FAQ)
Q1:为什么用 replaceState 不用 pushState?
避免每次筛选都进历史栈,否则后退会卡在筛选态。
Q2:排序后页面空白怎么防?
排序或筛选变化立即把页码重置为 1。
Q3:关键词输入要防抖吗?
要,输入类防抖 300ms,下拉与开关类可即时更新。