React Router 的 useSearchParams 全面指南:读写 URL 查询参数与源码级原理
在 React Router 中,URL 的查询字符串(query string)是页面状态的重要载体,例如搜索关键字、筛选条件、分页页码、标签页激活项等。useSearchParams 正是为这一场景设计的声明式 Hook:它返回当前 URL 的查询参数与其更新函数,任何一次更新都会触发一次标准的路由导航。本文以 useSearchParams 官方 API 文档 为主体,结合本仓库 useSearchParams 的实现源码、createSearchParams 与合并逻辑 以及 完整测试用例 展开讲解。读完后你将掌握:Hook 的完整签名、defaultInit 与函数式更新的准确语义、setSearchParams 的各类入参与导航选项、稳定引用与可变性陷阱,以及它在搜索表单、筛选面板等场景中的实战写法。
适用模式与导入方式
useSearchParams 同时适用于 React Router 的 framework、data、declarative 三种模式(原文档顶部标注了 [MODES: framework, data, declarative]),也就是说无论你使用 <BrowserRouter>/<MemoryRouter> 这类声明式路由、createBrowserRouter/RouterProvider 这类数据路由,还是基于 Vite 插件的完整框架模式,都可以直接使用该 Hook。
它从包的顶层入口导出:
import { useSearchParams } from "react-router";
在组件中使用时,它返回一个二元组,无需任何参数即可读取当前 URL 的查询参数:
export function SomeComponent() {
const [searchParams, setSearchParams] = useSearchParams();
// ...
}
Hook 签名与返回结构
原文档给出的完整 TypeScript 签名如下:
function useSearchParams(
defaultInit?: URLSearchParamsInit,
): [URLSearchParams, SetURLSearchParams]
- 返回的二元组(tuple)第一个元素:当前 URL 查询参数对应的原生
URLSearchParams实例。你可以直接用它的全套 API 读取参数:searchParams.get("tab")、searchParams.getAll("brand")、searchParams.has("q")、searchParams.toString()。 - 返回的二元组第二个元素:更新函数,类型为
SetURLSearchParams。调用它会触发一次导航,把浏览器地址栏与路由状态同步到新 URL。
从源码看,返回值的实现非常简洁(packages/react-router/lib/dom/lib.tsx):
let location = useLocation();
let searchParams = React.useMemo(
() =>
getSearchParamsForLocation(
location.search,
hasSetSearchParamsRef.current ? null : defaultSearchParamsRef.current,
),
[location.search],
);
let navigate = useNavigate();
let setSearchParams = React.useCallback<SetURLSearchParams>(
(nextInit, navigateOptions) => {
const newSearchParams = createSearchParams(
typeof nextInit === "function"
? nextInit(new URLSearchParams(searchParams))
: nextInit,
);
hasSetSearchParamsRef.current = true;
navigate("?" + newSearchParams, navigateOptions);
},
[navigate, searchParams],
);
return [searchParams, setSearchParams];
这一段代码几乎浓缩了该 Hook 的全部语义:查询参数由 useLocation() 派生而来、更新操作最终落到 navigate("?" + 新参数串),这正是“Setting the search params causes a navigation”的底层实现。
读取查询参数:tuple 的第一个元素
读取与普通 URLSearchParams 完全一致,例如:
let [searchParams] = useSearchParams();
searchParams.get("tab"); // 读取单个值,如 "1"
searchParams.getAll("brand"); // 读取同名多个值,如 ["nike", "reebok"]
searchParams.has("q"); // 判断键是否存在
searchParams.toString(); // 序列化为 "tab=1&brand=nike"
官方文档在 Notes 小节 中特别强调了一个关键特性:
searchParams是一个稳定引用(stable reference),因此可以放心地把它放进useEffect的依赖数组。
useEffect(() => {
console.log(searchParams.get("tab"));
}, [searchParams]);
这背后是源码中 React.useMemo(..., [location.search]) 的保证:只有当 location.search 实际发生变化时,searchParams 才会被重建,否则多次渲染拿到的是同一个对象引用,不会引起 useEffect 的重复执行。
陷阱:稳定引用同时意味着可变
稳定引用的另一面是它同样是可变的(mutable)。源码直接基于当前 location.search 构建并返回了该实例,你在组件内调用 searchParams.set(...) 会就地修改对象本身。一旦其他状态触发了组件重新渲染,而这个对象又没有被 useMemo 依据的 location.search 变化重新构建,你就会在渲染中读到被“擅自修改”的值,同时 URL 并不会反映这些改动——因为 URL 只有在调用 setSearchParams(即发起导航)时才会改变。因此,请勿在读取之外直接原地修改返回的 searchParams,正确的做法永远是把它交给 setSearchParams。
defaultInit 参数:初始化默认值
useSearchParams 的第一个可选参数 defaultInit?: URLSearchParamsInit 用于初始化默认查询参数。原文档强调:默认值只用于合并/兜底,首次渲染时它不会改变 URL。
URLSearchParamsInit 支持四种形态(类型定义见 packages/react-router/lib/dom/dom.ts):
type ParamKeyValuePair = [string, string];
type URLSearchParamsInit =
| string
| ParamKeyValuePair[]
| Record<string, string | string[]>
| URLSearchParams;
对应到初始化写法:
// ① 一个查询参数字符串
useSearchParams("?tab=1");
// ② 一个简写对象
useSearchParams({ tab: "1" });
// ③ 对象的键值可以是数组,表示同一键的多个值
useSearchParams({ brand: ["nike", "reebok"] });
// ④ 一个二元组数组
useSearchParams([["tab", "1"]]);
// ⑤ 一个 URLSearchParams 对象
useSearchParams(new URLSearchParams("?tab=1"));
注意写法 ③ 是 React Router 对原生 new URLSearchParams 的扩展:原生构造函数在对象形态下只接受 string 值,而 React Router 允许值本身是字符串数组,方便表达多值参数。
默认值的合并语义
默认值并非直接“覆盖”URL 参数,而是与 URL 中已有的查询参数做按 key 合并(只补缺、不覆盖)。这一步由 getSearchParamsForLocation 完成:
export function getSearchParamsForLocation(
locationSearch: string,
defaultSearchParams: URLSearchParams | null,
) {
let searchParams = createSearchParams(locationSearch);
if (defaultSearchParams) {
defaultSearchParams.forEach((_, key) => {
if (!searchParams.has(key)) {
defaultSearchParams.getAll(key).forEach((value) => {
searchParams.append(key, value);
});
}
});
}
return searchParams;
}
也就是说:URL 本身已带有的 key 以 URL 为准,只有 URL 中缺失的 key 才会被默认值补上。这一点在官方测试 search-params-test.tsx 中得到了验证:当 URL 为 /search?value=initial、默认值为 { a: "1", b: "2" } 时,searchParams.toString() 的结果是 value=initial&a=1&b=2——URL 上的 value 保持原样,缺失的 a、b 由默认值补齐。
值得留意的是,源码中这段默认值合并逻辑特意使用了 defaultSearchParams.forEach(...) 而非 defaultSearchParams.keys() 迭代,源码注释指出这是为了规避 Firefox 扩展环境下的已知 bug(参见 Bugzilla 1414602/1023984)。
何时不再合并默认值
实现中还维护了一个 hasSetSearchParamsRef 标记:一旦你调用过 setSearchParams,后续渲染就不再合并默认值(源码中传入 null)。这样设计的动机同样写在源码注释里:
一旦我们调用过
setSearchParams,新值就要占据优先地位,否则若某个参数有初始默认值,你将永远无法用setSearchParams({})把它移除。
对应的官方测试是 “allows removal of search params when a default is provided”(search-params-test.tsx):组件初始化 useSearchParams({ value: "initial" }),点击按钮执行 setSearchParams({}) 后,页面显示的当前值由 "initial" 变为 ""。
setSearchParams:tuple 的第二个元素
setSearchParams 是更新函数,它接受与 defaultInit 相同的入参类型,并且会触发一次指向新 URL 的导航(在数据路由/framework 模式下,这会走完整的路由导航流程,包括 loaders 重验证)。
let [searchParams, setSearchParams] = useSearchParams();
// ① 查询参数字符串
setSearchParams("?tab=1");
// ② 简写对象
setSearchParams({ tab: "1" });
// ③ 对象键为数组,写入同键多值(与原生 URLSearchParams 的行为不同)
setSearchParams({ brand: ["nike", "reebok"] });
// ④ 二元组数组
setSearchParams([["tab", "1"]]);
// ⑤ URLSearchParams 实例
setSearchParams(new URLSearchParams("?tab=1"));
注意语义上的差异:对 setSearchParams 而言,传入的值是导航的目标状态——它会用新参数整体替换当前的查询字符串(在调用一次之后不再叠加默认值),这与 defaultInit 的“合并补缺”语义不同。
类型定义中的完整签名
SetURLSearchParams 在源码 packages/react-router/lib/dom/lib.tsx 中的定义为:
export type SetURLSearchParams = (
nextInit?:
| URLSearchParamsInit
| ((prev: URLSearchParams) => URLSearchParamsInit),
navigateOpts?: NavigateOptions,
) => void;
可见它支持两个参数:第一个是新的参数(或函数式更新),第二个是 NavigateOptions 导航选项。
函数式更新(functional update)
setSearchParams 支持类似 React setState 的函数回调写法:
setSearchParams((searchParams) => {
searchParams.set("tab", "2");
return searchParams;
});
回调会收到当前的 URLSearchParams,你可以就地修改后返回它。从源码看,这一步先通过 nextInit(new URLSearchParams(searchParams)) 把当前参数浅拷贝成一份新的 URLSearchParams 交给回调,再经 createSearchParams 序列化后导航——注意这里拷贝的是调用时刻的 searchParams,回调内对其的修改只会体现在本次导航产生的 URL 上。
函数式写法在需要基于当前值做增量修改时非常顺手,官方测试 “updates searchParams when a function is provided to setSearchParams” 演示了典型用法(search-params-test.tsx):在既有 q 值上追加后缀并新增 new 键,一次导航同时完成两个修改。
⚠️ 函数式更新不支持 React 的 queueing 语义
这是官方文档明确给出的一条重要警告:
setSearchParams的函数回调版本不实现 ReactsetState的那套排队(queueing)逻辑。同一事件循环 tick 内的多次setSearchParams调用不会以前一次的结果为基准累积。如果你确实需要这种“基于上一个值连续累积”的行为,请自行用setState手动维护状态。
原因从源码可直观看到:每次调用都直接基于当前闭包捕获的 searchParams 计算新值并立刻发起导航,回调之间不存在 React setState 的自动批处理队列。
第二个参数 navigateOpts:导航选项
setSearchParams(params, navigateOpts) 的第二个参数是标准导航选项 NavigateOptions。其字段在 packages/react-router/lib/router/router.ts 中被定义为导航基类选项,主要包括:
| 选项 | 类型 | 说明 |
|---|---|---|
replace |
boolean |
是否用新条目替换当前历史记录,而不是压栈(默认 false) |
state |
any |
写入本次 History 条目的状态(可用 location.state 读取) |
preventScrollReset |
boolean |
配合 <ScrollRestoration> 时阻止导航完成后滚动复位(默认 false) |
relative |
"route" | "path" |
相对导航的解析基准(默认 "route") |
flushSync |
boolean |
是否对该导航的状态更新启用 flushSync |
viewTransition |
boolean |
是否为本导航启用 View Transition API |
defaultShouldRevalidate |
boolean |
设置本导航的默认重验证行为,可用于只想更新查询参数、不想触发 loaders 重验证的场景 |
mask |
To |
导航时向浏览器展示的“遮罩”位置 |
SetURLSearchParams 类型签名(见上文)允许在需要“静默替换历史”或“保留滚动位置”时传入这些选项。
实战模式:把查询参数当作受控搜索框
将 useSearchParams 与表单结合是官方测试展示的经典模式(search-params-test.tsx)。当用户提交时,把输入框的值写入查询参数,路由据此重新渲染,页面状态与 URL 保持一致,天然支持刷新保留、前进后退与分享链接:
function SearchPage() {
let queryRef = React.useRef<HTMLInputElement>(null);
let [searchParams, setSearchParams] = useSearchParams({ q: "" });
let query = searchParams.get("q")!;
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (queryRef.current) {
setSearchParams({ q: queryRef.current.value });
}
}
return (
<div>
<p>The current query is "{query}".</p>
<form onSubmit={handleSubmit}>
<input name="q" defaultValue={query} ref={queryRef} />
</form>
</div>
);
}
从官方测试“reads and writes the search string”可以看到完整链路:初始 URL /search?q=Michael+Jackson 时页面渲染 “The current query is `Michael Jackson`”(+ 被正确解码为空格);提交新值后 URL 更新、组件按新参数重渲染。
一些实用的编码建议:
- 需要使用多个筛选值/多选时,用对象键为数组的写法,如
setSearchParams({ brand: ["nike", "reebok"] }),读取侧配合searchParams.getAll("brand"); - 只想保留/清空某部分参数时,优先用函数式更新在拷贝上操作:
setSearchParams((prev) => { prev.delete("page"); return prev; }); - 不希望查询参数变化产生新的历史记录(如防抖搜索实时输入),可在第二个参数传
{ replace: true }; - 由函数式更新在拷贝上进行删除再返回,可以绕开“默认值无法被移除”的边界(参见上文
hasSetSearchParamsRef说明)。
浏览器兼容性:URLSearchParams 环境依赖
useSearchParams 依赖浏览器原生 URLSearchParams API。源码 packages/react-router/lib/dom/lib.tsx 在执行时会对该 API 的存在性做一次开发期警告:
你不能在不支持
URLSearchParamsAPI 的浏览器中使用useSearchParams。如果需要支持 IE11,建议引入 polyfill。
因此,若你的目标环境包含不支持 URLSearchParams 的老旧浏览器,需要预先加载相应 polyfill,否则该 Hook 无法正常工作。
底层工具函数:createSearchParams 与多值展开
createSearchParams 是整个机制的底层拼图(packages/react-router/lib/dom/dom.ts),它接收与 defaultInit 相同的四种形态并归一化为 URLSearchParams:
export function createSearchParams(
init: URLSearchParamsInit = "",
): URLSearchParams {
return new URLSearchParams(
typeof init === "string" ||
Array.isArray(init) ||
init instanceof URLSearchParams
? init
: Object.keys(init).reduce((memo, key) => {
let value = init[key];
return memo.concat(
Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]],
);
}, [] as ParamKeyValuePair[]),
);
}
关键点在于:当入参是普通对象时,它会逐 key 展开;若值是数组,则把一个 key 展开为多个 [key, value] 二元组——这正是 React Router 简写对象支持多值参数、且与原生 URLSearchParams 构造函数行为拉开差异的实现依据。官方文档示例同样印证了这一取舍:
// 与其这样写:
let searchParams = new URLSearchParams([
["sort", "name"],
["sort", "price"],
]);
// 不如这样写:
let searchParams = createSearchParams({
sort: ["name", "price"],
});
小结
useSearchParams 是 React Router 中以 URL 查询参数为“单一数据源”的核心 Hook:读取侧返回稳定引用、且默认值按“只补缺不覆盖”规则合并的 URLSearchParams;写入侧setSearchParams 接受字符串/对象/数组/URLSearchParams/函数式回调五种输入,并在调用后通过 navigate 触发导航,同时支持传入 replace、state、preventScrollReset、viewTransition 等标准导航选项。使用时注意三点:不要原地修改返回的 searchParams、函数式回调不同步支持 queueing 语义、首次调用 setSearchParams 后默认值将不再参与合并。
更完整的实践案例可继续阅读仓库中的 搜索参数实战指南、createSearchParams API 参考 与 Hooks 索引;组件外的编程式使用方式可参考 useNavigate。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00