React Router 的 createSearchParams 指南:让 URL 查询参数构造告别重复手写
createSearchParams 是 React Router 提供的一个轻量级工具函数,它用于创建一个 URLSearchParams 对象。本文围绕 docs/api/utils/createSearchParams.md 展开,说明它的设计动机、类型签名与底层实现,并演示它如何同 useSearchParams、href 等 React Router API 配合,快速构造可导航的查询字符串。读完本文,你将掌握用「对象字面量 + 数组值」描述多重查询参数的方法,并理解 React Router 内部是如何统一各种查询参数输入形态的。
适用模式:framework / data / declarative(框架模式、数据模式、声明式模式均可使用)。本文对应的官方 API 参考入口在 docs/api/utils/index.md 的 utils 分类中。
一、为什么需要 createSearchParams
浏览器原生提供的 new URLSearchParams(init) 已经足够强大,但当你想用「对象」形式一次性表达一个键对应多个取值时,它并不支持对象值;标准的做法是把每对键值写成二元组放进数组:
let searchParams = new URLSearchParams([
["sort", "name"],
["sort", "price"],
]);
这里同一键 sort 出现了两次,代码既重复又易错。createSearchParams 的职责就是消除这种重复:它在行为上与 new URLSearchParams(init) 完全一致,唯一的区别是——当传入对象字面量时,它额外允许用数组作为某个键的值,从而表达多重取值。
import { createSearchParams } from "react-router";
let searchParams = createSearchParams({
sort: ["name", "price"],
});
searchParams.toString(); // "sort=name&sort=price"
searchParams.getAll("sort"); // ["name", "price"]
这段示例正是 React Router 源码 JSDoc 中的原始用例(见 packages/react-router/lib/dom/dom.ts),它直观地说明了本函数存在的全部价值:多值查询参数的可读写法。
二、函数签名与参数说明
createSearchParams 的完整签名如下:
function createSearchParams(init: URLSearchParamsInit = ""): URLSearchParams
参数:init
init 是用于初始化 URL 查询参数的值,它并非原生 string 那么简单,而是 React Router 专门导出的一个联合类型 URLSearchParamsInit。从 packages/react-router/lib/dom/dom.ts 可以看到它的精确定义:
export type ParamKeyValuePair = [string, string];
export type URLSearchParamsInit =
| string // "sort=name&brand=nike"
| ParamKeyValuePair[] // [["sort", "name"], ["brand", "nike"]]
| Record<string, string | string[]> // { sort: ["name","price"] }
| URLSearchParams; // 另一个 URLSearchParams 实例
因此 init 实际上支持四种输入形态:
| 输入形态 | 示例 | 说明 |
|---|---|---|
string |
createSearchParams("tab=1") |
与原生字符串初始化行为一致,字符串内部的 ? 不会被特殊处理 |
| 键值对数组 | createSearchParams([["tab", "1"], ["brand", "nike"]]) |
完全等同原生 new URLSearchParams([...]) |
| 对象字面量 | createSearchParams({ brand: ["nike", "reebok"] }) |
字符串或字符串数组作为值,数组即表示多值,这是本函数相对原生的增强点 |
URLSearchParams |
createSearchParams(new URLSearchParams("?tab=1")) |
拷贝另一个实例 |
返回值:URLSearchParams
函数始终返回一个包含初始化后查询参数的标准 URLSearchParams 对象,可以继续调用 get、getAll、set、append、toString、forEach 等原生方法。
默认值
当不传 init(或显式传 undefined)时,参数默认值为空字符串 "",即返回一个空的查询参数对象:
createSearchParams().toString(); // ""
三、底层实现:对象如何被摊平成键值对数组
要理解 createSearchParams 的行为边界,值得看一眼它的完整实现(packages/react-router/lib/dom/dom.ts):
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[]),
);
}
这段代码揭示了三条可验证的事实:
- 透传策略:当
init是字符串、键值对数组或URLSearchParams实例时,函数直接把init原样交给原生构造器new URLSearchParams,不做任何额外处理,保证与原生行为逐字节一致。 - 对象摊平策略:只有当
init是对象时才会进入Object.keys(init).reduce(...)分支。每个键被读取出值后判断:若值是数组,则用value.map((v) => [key, v])把它展开成多组[key, v]二元组(键保持不变);否则生成单个[key, value]。最终把全部二元组合并成一个ParamKeyValuePair[]数组,再交给原生构造器。 - 顺序稳定性:由于对象使用
Object.keys遍历、数组.map按索引展开,键的先后顺序和数组内取值顺序都会原样保留在最终序列化结果里,这对排序参数(如sort)这类「顺序有意义」的场景很重要。
一个容易踩的坑是:对象形态下,值为数组的键会以重复出现的形式序列化,因此不要期待它变成 sort=name,price 这种逗号分隔的格式;需要那种格式时,你应该自行 join(",") 并赋一个字符串值。
四、四种 init 形态的完整实操对比
下面把同一组「两个 sort 值」用四种形态各写一遍,结果完全等价:
import { createSearchParams } from "react-router";
// 形态 1:字符串
createSearchParams("sort=name&sort=price");
// 形态 2:键值对数组(与原生一致)
createSearchParams([
["sort", "name"],
["sort", "price"],
]);
// 形态 3:对象字面量 + 数组值(推荐,可读性最好)
createSearchParams({ sort: ["name", "price"] });
// 形态 4:拷贝其它 URLSearchParams
createSearchParams(new URLSearchParams("sort=name&sort=price"));
四种写法最终都得到同样的对象:toString() 输出 sort=name&sort=price,getAll("sort") 返回 ["name", "price"]。
混合键也很自然——字符串键与多值数组键可以同处一个对象:
let searchParams = createSearchParams({
brand: ["nike", "reebok"],
tab: "1", // 单值键也可以直接给字符串
featured: "true",
});
searchParams.toString();
// "brand=nike&brand=reebok&tab=1&featured=true"
五、在 useSearchParams 与导航场景中配合使用
createSearchParams 最常见的用途并不是被单独调用,而是作为 React Router 查询参数体系的统一入口,内嵌在 useSearchParams 的默认值与更新逻辑中(实现见 packages/react-router/lib/dom/lib.tsx)。
useSearchParams 的签名是:
useSearchParams(defaultInit?: URLSearchParamsInit): [URLSearchParams, SetURLSearchParams]
可见其 defaultInit 参数的类型正是 URLSearchParamsInit——这意味着你在初始化默认值时,同样能享受对象数组值语法:
import { useSearchParams } from "react-router";
// 对象形式的多值默认参数
let [searchParams] = useSearchParams({
sort: ["name", "price"],
});
// 其它合法写法,见源码 JSDoc 示例
// useSearchParams("?tab=1");
// useSearchParams({ tab: "1" });
// useSearchParams({ brand: ["nike", "reebok"] });
// useSearchParams([["tab", "1"]]);
// useSearchParams(new URLSearchParams("?tab=1"));
在源码层面,useSearchParams 是这样消费 createSearchParams 的(packages/react-router/lib/dom/lib.tsx):
let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit));
let hasSetSearchParamsRef = React.useRef(false);
let location = useLocation();
let searchParams = React.useMemo(
() =>
getSearchParamsForLocation(
location.search,
hasSetSearchParamsRef.current ? null : defaultSearchParamsRef.current,
),
[location.search],
);
这里可以看到两条值得注意的工程细节:
- 默认值在首次渲染被立即转换为
URLSearchParams并存进useRef,之后每次渲染都复用同一实例,避免重复构建。 - 一旦调用过
setSearchParams,后续就会以null替代默认值参与合并,这正是为了让setSearchParams({})能真正清空参数——否则默认值会不断被合并回来。相关的合并逻辑getSearchParamsForLocation也定义在 packages/react-router/lib/dom/dom.ts,它只会把「当前 URL 中不存在的键」用默认值补上,已存在的键以 URL 实际值为准。
setSearchParams 与 createSearchParams 的关系同样直接。setSearchParams 的类型(packages/react-router/lib/dom/lib.tsx)是:
export type SetURLSearchParams = (
nextInit?:
| URLSearchParamsInit
| ((prev: URLSearchParams) => URLSearchParamsInit),
navigateOpts?: NavigateOptions,
) => void;
而在它的实现里(packages/react-router/lib/dom/lib.tsx),createSearchParams 承担了把各种输入归一化的职责,随后调用 navigate("?" + newSearchParams, navigateOptions) 触发一次带查询字符串的导航:
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],
);
因此,只要牢记 createSearchParams 支持的四种输入形态,也就等于掌握了 setSearchParams 与 useSearchParams(defaultInit) 的完整输入语法。一个将二者结合的分页筛选示例:
import { useSearchParams } from "react-router";
function ProductFilters() {
const [searchParams, setSearchParams] = useSearchParams();
// 读取多值:brand 可能同时命中多个
const brands = searchParams.getAll("brand");
function toggleBrand(brand: string) {
// 以函数形式基于上一状态改写,避免并发更新下互相覆盖
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
const list = next.getAll("brand");
if (list.includes(brand)) {
next.delete("brand");
list.filter((b) => b !== brand).forEach((b) => next.append("brand", b));
} else {
next.append("brand", brand);
}
return next;
});
}
// 一键重置:传对象形态,允许 set 覆盖
function reset() {
setSearchParams({ tab: "1" }, { preventScrollReset: true });
}
return (
<div>
<p>当前选中:{brands.join(", ")}</p>
<button onClick={() => toggleBrand("nike")}>切换 nike</button>
<button onClick={reset}>重置</button>
</div>
);
}
六、结合 href / useHref 使用
createSearchParams 返回的 URLSearchParams 也可直接与 useHref、href 这类链接构造工具配合,先生成带查询参数的完整 href,再传给 Link 等组件。完整的 hooks 与 utils 用法可参考 docs/api/hooks/useSearchParams.md、docs/api/hooks/useHref.md 与 docs/api/hooks/useLinkClickHandler.md。
import { createSearchParams } from "react-router";
import type { LinkProps } from "react-router";
// 在组件外构造:多值查询参数的可读对象写法
const resultsHref = {
pathname: "/products",
search: createSearchParams({ sort: ["price", "name"] }).toString(),
};
function ProductsLink(props: Partial<LinkProps>) {
return <Link to={resultsHref} {...props}>查看排序结果</Link>;
}
七、可用的导出入口与使用限制
createSearchParams 与类型 URLSearchParamsInit 从包的主入口统一导出。导出语句位于 packages/react-router/index.ts:
export { createSearchParams } from "./lib/dom/dom";
使用时只需:
import { createSearchParams, type URLSearchParamsInit } from "react-router";
需要留意的限制有两点:
- 依赖浏览器 URLSearchParams:函数最终依赖原生
URLSearchParams。源码中useSearchParams对此有明确的告警提示(packages/react-router/lib/dom/lib.tsx):若运行环境(如 IE11)不支持该 API,需要引入 polyfill 才能正常使用。 - 对象值的可接受类型:对象形态只接受
string或string[]作为值,URLSearchParamsInit的类型定义已经给出约束;传入非字符串内容(如数字)时,类型检查会直接报错。需要数字时请先显式转换:
createSearchParams({ page: String(2) }); // "page=2"
小结
createSearchParams在行为上与new URLSearchParams保持一致,并额外支持「对象 + 数组值」这一多值查询参数的高可读写法。- 其入参
URLSearchParamsInit覆盖字符串、键值对数组、对象、URLSearchParams四种形态;实现层面只对对象形态做数组值摊平,其余原样透传。 - 它同时是
useSearchParams(defaultInit)与setSearchParams(nextInit)的统一归一化入口,掌握它就等于掌握了 React Router 查询参数体系的输入语法,也可用于独立构造带查询串的 href。
想进一步了解 URL 与搜索参数的更多用法,可继续阅读 docs/api/utils/index.md 下的 parsePath、resolvePath、generatePath、useSearchParams 等相邻文档,或在 packages/react-router/lib/dom/dom.ts 中查看完整实现。
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 StartedRust0627
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