陌上人如玉
公子世无双

6.6 查询参数

6.6 查询参数

一句话:用 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 页数据」)

八、面试延伸

  1. 「useSearchParams 是什么?」
    它是 React Router 提供的 Hook,用于读写 URL 里的查询参数。它返回 [searchParams, setSearchParams]:searchParams 是一个 只读的 URLSearchParams 快照(提供 get / getAll / has / toString),setSearchParams 用于更新 URL(更新后会触发重渲染)。关键点:直接修改 searchParams 不会生效(它是快照),必须调用 setSearchParams。

  2. 「为什么要把状态放进 URL?」
    因为 URL 是「应用状态的序列化形式」,放进去能获得四种能力:① 刷新后保留(不丢状态);② 可分享(复制链接给别人看到同样的页面);③ 浏览器前进/后退(用户可以用后退键回到上一个筛选状态);④ 可收藏/可被外部读取(便于埋点与 SSR)。所以「筛选条件、分页页码、Tab 选中、搜索关键词」这类状态都应该放 URL,而不是 useState。

  3. 「setSearchParams 传对象和传函数有什么区别?」
    传对象是「替换」——没有提到的参数会被丢弃(如 setSearchParams({ page: '2' }) 会把 q、category 全删掉)。传函数是「增量更新」——回调接收当前参数(一个 URLSearchParams),你只修改关心的项,其余保留:setSearchParams(prev => { prev.set('page', '2'); return prev; })。推荐用函数形式,除非你确实想重置全部参数。

  4. 「怎么处理多值参数?」
    读取用 getAll(key)(返回数组),写入要注意set 会覆盖所有同名参数——所以「更新多值」的正确做法是「先 delete(key) 删除所有同名参数,再用 append 逐个添加」。用对象形式传数组在某些版本不生效,因此「函数形式 + delete + append」是最稳妥的写法(如多选标签筛选)。

  5. 「输入框绑定 URL 参数要注意什么?」
    最大的问题是历史记录污染——如果每次按键都 setSearchParams,就会「每个字符产生一条历史记录」,用户按后退会逐字符回退(体验极差)。两种解法:① 加 { replace: true }(只保留一条历史记录,最简单);② 本地 state + 防抖——输入用 useState,防抖 300ms 后再同步到 URL(更省渲染,适合大列表)。此外要处理「URL 变化(如后退)时把值同步回输入框」。

  6. 「URL 参数变化会触发重新请求吗?」
    不会自动触发——React Router 只负责「URL 与组件的对应关系」,不管数据。要重新请求需要自己把参数接入数据层:① TanStack Query——把参数放进 queryKey(queryKey: ['products', query]),参数变化 → key 变化 → 自动重新请求;② useEffect——把参数写进依赖数组;③ 数据路由的 loader——loader 会在每次导航(含查询参数变化)时重新执行。

  7. 「查询参数和 state 怎么选?」
    判断标准是「是否需要被分享/刷新恢复」:① 需要 → 放查询参数(?q=react&page=1)——可分享、可刷新、进日志(所以别放敏感信息);② 不需要(只是页面间传临时数据)→ 放 navigate 的 state——不污染 URL、无需序列化、但直接打开链接时是 undefined(必须有兜底)。特别提醒:敏感信息(token、身份证号)两种情况都不该放(URL 会进浏览器历史、服务器日志、Referer 头)。

  8. 「怎么处理『筛选变化时重置页码』?」
    在「更新筛选条件」的那一次 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。

赞(0) 打赏
未经允许不得转载:陌上寒 » 6.6 查询参数

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫