
一句话:用
useSearchParams()读写「URL 里的查询参数」(?a=1&b=2)——它返回一个只读的URLSearchParams快照与一个 setter;把「筛选、分页、Tab」这类状态放进 URL,能获得「可分享、可刷新、可回退」的能力。
一、基础用法
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get('q') ?? ''; // ⭐ 读取(注意 null)
const page = Number(searchParams.get('page') ?? '1');
const tags = searchParams.getAll('tag'); // ⭐ 多值
return (
<div>
<p>关键词:{keyword},第 {page} 页,标签:{tags.join(', ')}</p>
<button onClick={() => setSearchParams({ q: 'react', page: '1' })}>
搜索 react
</button>
</div>
);
}
⚠️ 两个必须知道的事实:
① searchParams 是【只读快照】
直接改它不会生效:
❌ searchParams.set('q', 'x');
✅ setSearchParams({ q: 'x' });
② get() 返回的是 string | null
❌ const page = searchParams.get('page'); // string | null
✅ const page = Number(searchParams.get('page') ?? '1');
二、与 URLSearchParams 的关系
// searchParams 就是一个 URLSearchParams 实例
const [searchParams, setSearchParams] = useSearchParams();
// 常用的读取方法
searchParams.get('q'); // 'react' | null(第一个值)
searchParams.getAll('tag'); // ['a', 'b'](所有同名值)
searchParams.has('q'); // boolean
searchParams.toString(); // 'q=react&page=1'(序列化)
searchParams.size; // 参数个数(较新的 API)
searchParams.entries(); // 迭代器(可用于 Object.fromEntries)
// 转成普通对象
const params = Object.fromEntries(searchParams); // ⚠️ 多值会被覆盖
const all = Object.fromEntries(
[...new Set(searchParams.keys())].map((k) => [k, searchParams.getAll(k)])
); // ✅ 保留多值
三、更新参数的三种方式
const [searchParams, setSearchParams] = useSearchParams();
// ① 传对象(会「替换」整个查询串)
setSearchParams({ q: 'react', page: '1' });
// → ?q=react&page=1(其它已有参数会丢失 ⚠️)
// ② 传字符串或 URLSearchParams
setSearchParams('q=react&page=1');
setSearchParams(new URLSearchParams({ q: 'react' }));
// ③ 传函数(基于当前参数「增量更新」)—— 推荐 ⭐
setSearchParams((prev) => {
prev.set('q', 'react');
prev.set('page', '1'); // 保留其它已有参数
return prev;
});
// ④ 删除某个参数
setSearchParams((prev) => {
prev.delete('q');
return prev;
});
| 方式 | 行为 | 建议 |
|---|---|---|
| 传对象 | 替换(丢弃未提及的参数) | 只在「明确要重置」时用 |
传字符串 / URLSearchParams |
替换 | 少用 |
传函数(基于 prev 修改) |
增量更新 | ✅ 推荐 |
// ⑤ 第二个参数支持 navigate 选项(如 replace)
setSearchParams({ page: '2' }, { replace: true });
// ⭐ 不留下历史记录(适合「输入驱动的实时筛选」)
四、多值参数
// URL: ?tag=react&tag=vue&tag=ts
const tags = searchParams.getAll('tag'); // ['react', 'vue', 'ts']
// ✅ 多选筛选的完整实现
function TagFilter() {
const [searchParams, setSearchParams] = useSearchParams();
const selected = searchParams.getAll('tag');
const toggleTag = (tag: string) => {
setSearchParams((prev) => {
const current = prev.getAll('tag');
prev.delete('tag'); // ⚠️ 先删掉所有同名参数
const next = current.includes(tag)
? current.filter((t) => t !== tag)
: [...current, tag];
next.forEach((t) => prev.append('tag', t)); // ⭐ 再逐个追加
return prev;
});
};
return (
<>
{ALL_TAGS.map((tag) => (
<label key={tag}>
<input type="checkbox" checked={selected.includes(tag)} onChange={() => toggleTag(tag)} />
{tag}
</label>
))}
</>
);
}
⚠️ 多值参数的坑:
· set(key, value) 会「覆盖」所有同名参数 → 多值场景要用 append
· 因此「更新多值时」必须先 delete 再 append
· 用对象形式 setSearchParams({ tag: ['a','b'] }) 在有些版本不生效
→ 稳妥做法是用「函数形式 + append」
五、为什么把状态放 URL(对比 useState)
| 能力 | useState |
URL(useSearchParams) |
|---|---|---|
| 刷新后保留 | ❌ 丢失 | ✅ 保留 |
| 可分享(复制链接) | ❌ | ✅ |
| 浏览器前进/后退 | ❌ | ✅ |
| 可被收藏 | ❌ | ✅ |
| 服务端/前端可读 | ❌ | ✅(便于 SSR 与分析) |
| 写入成本 | 低 | 稍高(会改动历史/URL) |
// ✅ 典型场景:把「筛选 + 分页 + 排序」全部放 URL
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const query = {
category: searchParams.get('category') ?? 'all',
sort: searchParams.get('sort') ?? 'default',
page: Number(searchParams.get('page') ?? '1'),
keyword: searchParams.get('q') ?? '',
};
// ⭐ 请求的 key 依赖 URL(所以「复制链接给别人」看到的是同一份数据)
const { data, isLoading } = useQuery({
queryKey: ['products', query],
queryFn: () => api.getProducts(query),
placeholderData: (prev) => prev, // 翻页时保留上一页,避免闪白
});
const updateQuery = (patch: Partial<typeof query>) => {
setSearchParams((prev) => {
Object.entries(patch).forEach(([k, v]) => {
if (v === undefined || v === '' || v === null) prev.delete(k);
else prev.set(k, String(v));
});
return prev;
});
// ⭐ 如果「筛选变化要重置页码」,在 patch 里显式带上 page: '1'
};
return (
<>
<FilterBar value={query} onChange={updateQuery} />
{isLoading ? <Skeleton /> : <Grid products={data?.items ?? []} />}
<Pagination page={query.page} onChange={(page) => updateQuery({ page })} />
</>
);
}
判断「该不该放 URL」的三个问题:
① 这个状态需要被「分享/收藏」吗?→ 需要就放 URL
② 这个状态刷新后需要保留吗?→ 需要就放 URL
③ 这个状态需要支持「浏览器后退」吗?→ 需要就放 URL
符合其中任意一条 → URL;都不符合 → useState(更轻)
六、与受控输入结合(重要细节)
// ⚠️ 直接把输入框的值绑定到 URL 会导致「每敲一个字都新增一条历史记录」
function BadSearch() {
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get('q') ?? '';
return (
<input
value={keyword}
onChange={(e) => setSearchParams({ q: e.target.value })} // ❌ 每个字符一条历史
/>
);
}
// ✅ 方案一:用本地 state 承载输入,防抖后再同步到 URL
function GoodSearch() {
const [searchParams, setSearchParams] = useSearchParams();
const urlKeyword = searchParams.get('q') ?? '';
const [input, setInput] = useState(urlKeyword);
// URL 变化(如点了后退)时同步回输入框
useEffect(() => {
setInput(urlKeyword);
}, [urlKeyword]);
// 输入防抖 → 同步到 URL
const debounced = useDebouncedValue(input, 300);
useEffect(() => {
setSearchParams(
(prev) => {
if (debounced) prev.set('q', debounced);
else prev.delete('q');
prev.set('page', '1');
return prev;
},
{ replace: true } // ⭐ 不污染历史
);
}, [debounced, setSearchParams]);
return <input value={input} onChange={(e) => setInput(e.target.value)} />;
}
// ✅ 方案二:用 replace 选项(比方案一简单,但没有防抖)
setSearchParams({ q: value }, { replace: true });
// ⭐ 只保留一条历史记录,后退不会逐字符回退
// ⚠️ 但每次输入都会触发 URL 更新与重渲染 → 大列表时配合 useDeferredValue
七、常见坑
// ① 直接修改 searchParams(只读快照)
searchParams.set('q', 'x'); // ❌ URL 不变
// ② 用对象形式 setSearchParams → 丢失其它参数
setSearchParams({ page: '2' }); // ⚠️ q / category 都丢了
setSearchParams((prev) => { prev.set('page', '2'); return prev; }); // ✅
// ③ 忘了 get() 可能返回 null
const page = +searchParams.get('page'); // ⚠️ null → 0(NaN 风险)
const page2 = Number(searchParams.get('page') ?? '1'); // ✅
// ④ 每次输入都新增历史记录
setSearchParams({ q: v }); // ⚠️ 没有 replace
setSearchParams({ q: v }, { replace: true }); // ✅
// ⑤ 多值参数用 set 而不是 append
searchParams.set('tag', 'react'); // ⚠️ 覆盖了其它 tag
searchParams.append('tag', 'react'); // ✅
// ⑥ 把「敏感信息」放查询参数(会进日志、Referer)
?token=xxx // ❌
// ⑦ 认为「URL 参数变化会自动重新请求」
// → 需要把参数放进 queryKey / useEffect 依赖
// ⑧ 用 Object.fromEntries 丢掉多值参数
// ⑨ 在「渲染中」调用 setSearchParams(副作用)
setSearchParams({ q }); // ❌ 应放在事件处理/Effect 里
// ⑩ 参数值没有编码(空格、&、# 破坏 URL)
setSearchParams({ q: 'a&b' }); // ✅ URLSearchParams 会自动编码
navigate(`/search?q=${raw}`); // ⚠️ 手动拼接时要 encodeURIComponent
// ⑪ 与 useState 双份状态(URL 一份 + state 一份)→ 不同步
// ⑫ 忘记「筛选变化要重置页码」(出现「第 5 页但只有 2 页数据」)
八、面试延伸
- 「
useSearchParams是什么?」
它是 React Router 提供的 Hook,用于读写 URL 里的查询参数。它返回[searchParams, setSearchParams]:searchParams是一个 只读的URLSearchParams快照(提供get/getAll/has/toString),setSearchParams用于更新 URL(更新后会触发重渲染)。关键点:直接修改searchParams不会生效(它是快照),必须调用setSearchParams。 -
「为什么要把状态放进 URL?」
因为 URL 是「应用状态的序列化形式」,放进去能获得四种能力:① 刷新后保留(不丢状态);② 可分享(复制链接给别人看到同样的页面);③ 浏览器前进/后退(用户可以用后退键回到上一个筛选状态);④ 可收藏/可被外部读取(便于埋点与 SSR)。所以「筛选条件、分页页码、Tab 选中、搜索关键词」这类状态都应该放 URL,而不是useState。 -
「
setSearchParams传对象和传函数有什么区别?」
传对象是「替换」——没有提到的参数会被丢弃(如setSearchParams({ page: '2' })会把q、category全删掉)。传函数是「增量更新」——回调接收当前参数(一个URLSearchParams),你只修改关心的项,其余保留:setSearchParams(prev => { prev.set('page', '2'); return prev; })。推荐用函数形式,除非你确实想重置全部参数。 -
「怎么处理多值参数?」
读取用getAll(key)(返回数组),写入要注意set会覆盖所有同名参数——所以「更新多值」的正确做法是「先delete(key)删除所有同名参数,再用append逐个添加」。用对象形式传数组在某些版本不生效,因此「函数形式 +delete+append」是最稳妥的写法(如多选标签筛选)。 -
「输入框绑定 URL 参数要注意什么?」
最大的问题是历史记录污染——如果每次按键都setSearchParams,就会「每个字符产生一条历史记录」,用户按后退会逐字符回退(体验极差)。两种解法:① 加{ replace: true }(只保留一条历史记录,最简单);② 本地 state + 防抖——输入用useState,防抖 300ms 后再同步到 URL(更省渲染,适合大列表)。此外要处理「URL 变化(如后退)时把值同步回输入框」。 -
「URL 参数变化会触发重新请求吗?」
不会自动触发——React Router 只负责「URL 与组件的对应关系」,不管数据。要重新请求需要自己把参数接入数据层:① TanStack Query——把参数放进queryKey(queryKey: ['products', query]),参数变化 → key 变化 → 自动重新请求;②useEffect——把参数写进依赖数组;③ 数据路由的loader——loader会在每次导航(含查询参数变化)时重新执行。 -
「查询参数和
state怎么选?」
判断标准是「是否需要被分享/刷新恢复」:① 需要 → 放查询参数(?q=react&page=1)——可分享、可刷新、进日志(所以别放敏感信息);② 不需要(只是页面间传临时数据)→ 放navigate的state——不污染 URL、无需序列化、但直接打开链接时是undefined(必须有兜底)。特别提醒:敏感信息(token、身份证号)两种情况都不该放(URL 会进浏览器历史、服务器日志、Referer 头)。 -
「怎么处理『筛选变化时重置页码』?」
在「更新筛选条件」的那一次setSearchParams里同时把page设为1。常见的封装是「一个updateQuery(patch)函数」,它统一处理「删除空值」与「重置页码」:调用updateQuery({ category: 'books' })时自动带上page: '1'。如果不处理,就会出现「用户在第 5 页把分类改成只有 2 页数据的新分类」这类尴尬情况。
一句话速记
useSearchParams()读写 URL 查询参数,searchParams是只读快照(get/getAll/has/toString),get返回string | null(要自己转数字);更新用setSearchParams——传对象是「替换」(会丢参数)、传函数是「增量更新」(推荐),第二个参数可传{ replace: true };多值用getAll+delete再append;把「筛选/分页/排序/Tab」放 URL 能获得「可分享、可刷新、可回退」,但输入框绑定 URL 要replace或防抖(否则历史记录爆炸);URL 变化不会自动请求(要放进queryKey/依赖/loader);敏感信息不要放 URL。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!