类型化状态,生活在 URL 中
useUrlState 是将自身写入查询字符串的 React 状态。对象、数组和日期保持其类型,每个状态都是一个可分享的链接,并在重新加载后仍然存在——无需 provider,也无需样板代码。
- ~2 KB gzip 压缩
- 零依赖
- TypeScript 优先
- Next.js / react-router / Remix
- MIT
npm i state-in-urluseUrlState — 在线演示: react-router
在下方输入——观察 URL 亮起来
从 URL 读取——无需 props、无需 context,类型和结构得以保留
{name: stringage: undefinedundefinedagree_to_terms: falsebooleantags: []}
相同的 API,三种路由器
快速开始
export const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
// use `Type` not `Interface`!
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: { id: string; value: { text: string; time: Date } }[];
};import { useUrlState } from 'state-in-url/react-router';
// for react-router v6
// import { useUrlState } from 'state-in-url/react-router6';
import { form } from './form';
// One hook per feature - the whole API for this state
export const useFormState = () => useUrlState(form);import { useFormState } from './useFormState';
export const ComponentA = () => {
// see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState
const { urlState, setUrl, setState } = useFormState();
return <>
<input
id="name"
value={urlState.name}
onChange={(ev) => setUrl({ name: ev.target.value })}
/>
// OR can update state immediately but sync change to url as needed
<input
value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
onBlur={() => setUrl()}
/>
<button onClick={() => setUrl((curr, initial) => initial)}>
Reset
</button>
</>
};import { useFormState } from './useFormState';
export const ComponentB = () => {
// same state as ComponentA - no props, no context
const { urlState } = useFormState();
// will be defaultValue from `form` if not in url, no need to check
return <div>name: {urlState.name}</div>
};import React from 'react';
import { useUrlState } from 'state-in-url/react-router';
import { form } from './form';
export const useFormState = () => {
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form);
// first navigation will push new history entry
// all following will just replace that entry
// this way will have history with only 2 entries - ['/url', '/url?key=param']
const replace = React.useRef(false);
const setUrl = React.useCallback((
state: Parameters<typeof setUrlBase>[0],
opts?: Parameters<typeof setUrlBase>[1]
) => {
setUrlBase(state, { replace: replace.current, ...opts });
replace.current = true;
}, [setUrlBase]);
return { urlState, setUrl, resetUrl: reset };
};state-in-url vs nuqs
在找 nuqs 的替代品?两者都把带类型的状态存进查询字符串;区别在于需要多少配置,以及值可以是什么。
| 对比项 | state-in-url | nuqs |
|---|---|---|
| 配置 | 无需配置——导入 hook 即可使用 | 需要用适配器组件包裹应用 |
| 状态形态 | 一个带类型的对象,用法类似 React.useState | 按键存值,每个键都要声明解析器 |
| 跨组件复用 | 把 hook 包一次——所有组件共享状态,无需 props | 需要自己围绕解析器映射抽一个 hook |
| 嵌套对象和数组 | 内置支持——结构和类型都保留 | JSON 解析器加自己写的运行时校验 |
| 日期 | 自动保留 | 内置解析器,需逐键声明 |
| 体积(完整导入) | 约 2.9 KB gzip | 约 6.7 KB gzip |
| 运行时依赖 | 无 | 1 个 |
| 路由器 | Next.js、React Router v6/v7、Remix,纯 JS 辅助函数 | Next.js、React Router、Remix、TanStack Router、纯 React |
体积说明:整库导入,esbuild minify + gzip,2026 年 8 月对照 nuqs 2.10.1 测得。
nuqs 也是一个不错的库——如果你希望每个值都是一条可读的查询参数,或正在用 TanStack Router,就选它。想把整个带类型的对象放进 URL、零配置上手,就选 state-in-url。
为什么选择 state-in-url?
URL 状态库已经存在,但大多数要么设置繁琐,要么能存储的内容有限。 state-in-url 的目标就是开箱即用:提供镜像 React.useState 的 API,以 URL 作为存储。
无需样板代码即可存储状态、构建深层链接,并在不相关的客户端组件之间共享数据——无需 provider。结构和类型端到端得以保留: Date 进去, Date 出来。
以测试优先的方式构建,单元测试和跨浏览器 e2e 套件在每次提交时运行。
Next.js:无需 Suspense 边界
该 hook 从不调用 useSearchParams,因此使用它的组件无需包裹在 Suspense 中,也不会让页面退出预渲染——PPR 和 cacheComponents 也在内。它直接读取 URL 并跟踪之后的每一次变更,包括来自一段对它一无所知的代码的 history.pushState 。
不用 Next.js 或 react-router?
这些 encodeState / decodeState 辅助函数可用于任何框架或纯 JS——hook 只是它们之上的一层便利封装。
来看看 GitHub 页面 —— 一个 star 会带来很大帮助。
分享给其他开发者

