首页
/ React Router 的 useSearchParams 全面指南:读写 URL 查询参数与源码级原理

React Router 的 useSearchParams 全面指南:读写 URL 查询参数与源码级原理

2026-09-07 22:09:59作者:柏廷章Berta

在 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 保持原样,缺失的 ab 由默认值补齐。

值得留意的是,源码中这段默认值合并逻辑特意使用了 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 的函数回调版本不实现 React setState 的那套排队(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 的存在性做一次开发期警告:

你不能在不支持 URLSearchParams API 的浏览器中使用 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 触发导航,同时支持传入 replacestatepreventScrollResetviewTransition 等标准导航选项。使用时注意三点:不要原地修改返回的 searchParams、函数式回调不同步支持 queueing 语义、首次调用 setSearchParams 后默认值将不再参与合并。

更完整的实践案例可继续阅读仓库中的 搜索参数实战指南createSearchParams API 参考Hooks 索引;组件外的编程式使用方式可参考 useNavigate

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388