search-params
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseURL Search Param State Management
URL搜索参数状态管理
Decision Framework
决策框架
When updating the URL (search params or hash), choose between replace and push based on whether the change represents in-page state or a user-navigable step:
| Change type | Examples | History behavior |
|---|---|---|
| In-page state | Filters, sort, pagination, tab switches, search queries | |
| Navigable step | Wizard progression, multi-step forms | |
| Unsure? | Ask the developer before choosing |
Why this matters: Pushing in-page state changes clutters the browser history. Users clicking "back" expect to leave the page, not undo a filter toggle. This is the #1 cause of "back button is broken" bugs.
更新URL(搜索参数或哈希)时,需根据变更代表的是页面内状态还是用户可导航步骤,在replace和push之间做出选择:
| 变更类型 | 示例 | 历史记录行为 |
|---|---|---|
| 页面内状态 | 筛选器、排序、分页、标签切换、搜索查询 | |
| 可导航步骤 | 向导流程推进、多步骤表单 | |
| 不确定? | 选择前咨询开发人员 |
重要性说明: 将页面内状态变更加入历史记录会使浏览器历史记录杂乱。用户点击“后退”按钮时期望离开当前页面,而非撤销筛选器切换操作。这是“后退按钮失效”类Bug的头号诱因。
Correct Patterns
正确实践
Single search param - use useSearchParamState
(preferred)
useSearchParamState单个搜索参数 - 使用useSearchParamState
(推荐)
useSearchParamStateThis hook validates with Zod and always uses internally, so you get correct history behavior for free.
replace: truetsx
import { useSearchParamState } from '@app/hooks/useSearchParamState';
import { z } from 'zod';
const TabSchema = z.enum(['overview', 'details', 'settings']);
const [activeTab, setActiveTab] = useSearchParamState('tab', TabSchema, 'overview');Key file:
src/app/src/hooks/useSearchParamState.ts该钩子通过Zod进行验证,并且内部始终使用,因此你可以自动获得正确的历史记录行为。
replace: truetsx
import { useSearchParamState } from '@app/hooks/useSearchParamState';
import { z } from 'zod';
const TabSchema = z.enum(['overview', 'details', 'settings']);
const [activeTab, setActiveTab] = useSearchParamState('tab', TabSchema, 'overview');关键文件:
src/app/src/hooks/useSearchParamState.tsMultiple search params - use setSearchParams
with replace: true
setSearchParamsreplace: true多个搜索参数 - 使用setSearchParams
并传入replace: true
setSearchParamsreplace: trueWhen updating multiple params at once, use directly but always pass for in-page state:
setSearchParams{ replace: true }tsx
const [searchParams, setSearchParams] = useSearchParams();
// Updating filters (in-page state -> replace)
setSearchParams(
(params) => {
params.set('status', 'active');
params.set('sort', 'name');
return params;
},
{ replace: true },
);同时更新多个参数时,直接使用,但对于页面内状态变更,务必传入:
setSearchParams{ replace: true }tsx
const [searchParams, setSearchParams] = useSearchParams();
// 更新筛选器(页面内状态 -> 使用replace)
setSearchParams(
(params) => {
params.set('status', 'active');
params.set('sort', 'name');
return params;
},
{ replace: true },
);Hash-based navigation - navigate()
with replace or push
navigate()基于哈希的导航 - 结合replace或push使用navigate()
navigate()For wizard/multi-step flows where back button should traverse steps, use push (the default):
tsx
// Wizard step navigation - push so back button works between steps
// See: src/app/src/pages/redteam/setup/page.tsx
const updateHash = (newStep: string) => {
navigate(`#${newStep}`); // push (default) - intentional
};For hash changes that represent in-page state, use replace:
tsx
// Tab switch on a detail page - replace to avoid history clutter
navigate(`#${section}`, { replace: true });对于后退按钮应能遍历步骤的向导/多步骤流程,使用push(默认行为):
tsx
// 向导步骤导航 - 使用push使后退按钮可在步骤间切换
// 参考:src/app/src/pages/redteam/setup/page.tsx
const updateHash = (newStep: string) => {
navigate(`#${newStep}`); // push(默认)- 有意为之
};对于代表页面内状态的哈希变更,使用replace:
tsx
// 详情页标签切换 - 使用replace避免历史记录杂乱
navigate(`#${section}`, { replace: true });URL normalization after save
保存后的URL标准化
When the URL needs to be updated to include a new ID after a create/save operation (not a user action), use replace:
tsx
// After first save, update URL to include new ID without adding history entry
navigate(`/evals/${newConfigId}`, { replace: true });当创建/保存操作后需要更新URL以包含新ID(非用户主动操作)时,使用replace:
tsx
// 首次保存后,更新URL以包含新ID,不添加历史记录条目
navigate(`/evals/${newConfigId}`, { replace: true });Anti-Patterns
反模式
Pushing in-page state changes (breaks back button)
将页面内状态变更加入历史记录(破坏后退按钮)
tsx
// WRONG - every filter change adds a history entry
setSearchParams((params) => {
params.set('filter', value);
return params;
});
// WRONG - navigate without replace for state change
navigate(`?tab=${newTab}`);tsx
// 错误 - 每次筛选器变更都会添加一条历史记录
setSearchParams((params) => {
params.set('filter', value);
return params;
});
// 错误 - 状态变更时调用navigate未传入replace
navigate(`?tab=${newTab}`);Using raw useSearchParams
for a single param without validation
useSearchParams未验证的情况下对单个参数使用原生useSearchParams
useSearchParamstsx
// WRONG - no validation, easy to forget { replace: true }
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get('tab');
const setTab = (v: string) => {
setSearchParams((p) => {
p.set('tab', v);
return p;
});
};
// RIGHT - use the hook instead
const [tab, setTab] = useSearchParamState('tab', TabSchema, 'overview');tsx
// 错误 - 无验证,容易忘记传入{ replace: true }
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get('tab');
const setTab = (v: string) => {
setSearchParams((p) => {
p.set('tab', v);
return p;
});
};
// 正确 - 使用钩子替代
const [tab, setTab] = useSearchParamState('tab', TabSchema, 'overview');Using empty strings instead of null
使用空字符串而非null
tsx
// WRONG - useSearchParamState will throw an invariant error
setTab('');
// RIGHT - use null to clear a param
setTab(null);tsx
// 错误 - useSearchParamState会抛出不变量错误
setTab('');
// 正确 - 使用null清除参数
setTab(null);Key Files
关键文件
- - primary hook (uses replace internally)
src/app/src/hooks/useSearchParamState.ts - - example of correct
src/app/src/pages/eval/components/ResultsView.tsxusage{ replace: true } - - example of intentional push for wizard steps
src/app/src/pages/redteam/setup/page.tsx
- - 核心钩子(内部使用replace)
src/app/src/hooks/useSearchParamState.ts - - 正确使用
src/app/src/pages/eval/components/ResultsView.tsx的示例{ replace: true } - - 向导流程中有意使用push的示例
src/app/src/pages/redteam/setup/page.tsx