state-in-url
v8.0.2
Published
Store state in URL as in object, types and structure are preserved, with TS validation. Same API as React.useState, wthout any hasssle or boilerplate. Next.js@14-16, react-router@6-7, remix@2, and Astro.
Downloads
23,729
Maintainers
Keywords
Readme
English · 简体中文 · 日本語 · 한국어 · Русский · Español · Português (BR) · Français · Tiếng Việt
State in url

演示
演示 | 镜像链接
URI 大小限制,最多 12KB 是安全的
添加一个 并关注我以支持该项目!
非常欢迎您在讨论区分享您的反馈/意见
如果对您有用,请分享。 X.com LinkedIn FB VK
为什么使用 state-in-url?
在查询参数中存储任何用户状态;想象一下在浏览器 URL 中使用 JSON。所有这些都保持数据的类型和结构,例如数字将被解码为数字而不是字符串,日期作为日期,支持对象和数组。 简单明了、快速,并提供静态 TypeScript 验证。轻松实现深层链接(又名 URL 同步)。
包含用于 Next.js、react-router、Remix 和 Astro 的 useUrlState hook,以及用于任何其他 JS 框架的辅助函数。
由于现代浏览器支持巨大的 URL,用户也不关心查询字符串(这是一个全选和复制/粘贴的工作流程)。
是时候使用查询字符串进行状态管理了,正如它最初的用途。 这个库为您完成所有繁琐的工作。
这个库是 NUQS 的一个很好的替代品。
使用场景
- 在 URL 中存储未保存的用户表单或页面筛选器
- 将 URL 与 React 状态同步
- 只需在不相关的客户端组件之间同步数据,而无需触及 URI
- 带有应用程序状态的可共享 URL(深层链接、URL 状态同步)
- 轻松实现跨页面重新加载的状态持久化
特性
- 🧩 简单: 无需 providers、reducers、样板代码或新概念,API 类似于
React.useState - 📘 TypeScript 验证/自动完成: 状态只是一个对象,根据 TypeScript 定义在 IDE/测试中自动静态验证
- ✨ 复杂数据: 嵌套对象、日期和数组,与 JSON 工作方式相同,但在 URL 中
- ☂ 默认值: 如果参数不在 url 中,为您提供默认值
- ⌨ 有序: 所有可能的值在开始时定义,防止您获取不存在的键
- 兼容: 将保持第三方查询参数不变
- 灵活: 可以在同一页面上使用多个状态对象,只需使用不同的键
- 快速: 最少的重新渲染,大约1ms来编码和解码大对象
- 服务器端渲染: 可以在服务器组件中使用,支持 Next.js 14、15 和 16
- 轻量级: 零依赖,库小于 2KB
- 开发体验: 良好的开发者体验、文档、JSDoc 注释和示例
- 框架灵活性: 为
Next.js、react-router、Remix和Astro提供 hooks,以及用于其他框架或纯 JS 的辅助函数 - 经过充分测试: Chrome/Firefox/Safari 的单元测试和 Playwright 测试
- 宽松许可证: MIT
与 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、Astro,纯 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。
完整对比——同一个功能在两个库里的写法、其他替代品(TanStack Router、use-query-params)以及迁移说明——见 https://state-in-url.dev/vs/nuqs。
目录
- State in url
- 演示
安装
1. 安装包
# npm
npm install --save state-in-url
# yarn
yarn add state-in-url
# pnpm
pnpm add state-in-url2. 编辑 tsconfig.json
在 tsconfig.json 的 compilerOptions 中设置 "moduleResolution": "Bundler",或 "moduleResolution": "Node16",或 "moduleResolution": "NodeNext"。
可能需要设置 "module": "ES2022",或 "module": "ESNext"
3. 说明:本包仅以 ESM 方式解析
Next.js、Vite、Astro 和 Remix 无需任何配置即可打包它;在 require(esm) 已无需标志的
Node ^20.19 或 >=22.12 上,纯 Node 的 require() 同样可用。
dist/ 中还会附带一份扩展名为 .cjs 的 CommonJS 构建产物,但 exports 映射并不指向它,
因此解析不会意外落到它上面。它是为边缘情况准备的——较旧的 Node,或者不想加标志的
Jest——只能通过明确的路径引用。
无论哪种方式,Jest 都需要一行配置——参见注意事项第 5 条。
与 AI 编程代理一起使用
state-in-url 附带适用于 @tanstack/intent 的技能文件,因此 AI 代理(Claude Code、Cursor、Copilot、Codex 等)在使用该库时会加载正确的模式并避免常见错误。安装 state-in-url 后,在项目中运行一次:
npx @tanstack/intent@latest install这将使你已安装的代理能够从 node_modules/state-in-url/skills/ 发现技能。使用 npx @tanstack/intent@latest list 列出可用技能。
useUrlState
主 hook,接受初始状态作为参数,返回状态对象、更新 url 的回调以及仅更新状态的回调。
使用相同 state 对象的所有组件都会自动同步。
用于 Next.js 的 useUrlState hook
使用示例
基础用法
- 使用默认值定义状态形状
// userState.ts
// 只有与默认值不同的参数才会进入 url。
export const userState: UserState = { name: '', age: 0 }
// 使用 `Type` 而不是 `Interface`!
type UserState = { name: string, age: number }- 导入并使用
'use client'
import { useUrlState } from 'state-in-url/next';
import { userState } from './userState';
function MyComponent() {
// 可以传递 `replace` 参数,它控制 `setUrl` 是否使用 `router.push` 或 `router.replace`,默认 replace=true
// 可以从服务器组件传递 `searchParams`,如果需要在服务器组件中获取数据,则传递 `useHistory: false`
const { urlState, setUrl, setState } = useUrlState(userState);
return (
<div>
// 如果 url 为空,urlState.name 将返回 `userState` 的默认值
<input value={urlState.name}
// 与 React.useState 相同的 API,例如 setUrl(currVal => currVal + 1)
onChange={(ev) => setUrl({ name: ev.target.value }) }
/>
<input value={urlState.age}
onChange={(ev) => setUrl({ age: +ev.target.value }) }
/>
<input value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// 可以立即更新状态,但根据需要将更改同步到 url
onBlur={() => setUrl()}
/>
<button onClick={() => setUrl((_, initial) => initial)}>
Reset
</button>
</div>
)
}使用服务器端渲染
export default async function Home({ searchParams }: { searchParams: object }) {
return (
<Form searchParams={searchParams} />
)
}
// Form.tsx
'use client'
import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
const Form = ({ searchParams }: { searchParams: object }) => {
const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
}在 layout 组件中使用 hook
// 添加到适当的 `layout.tsx`
export const runtime = 'edge';
// middleware.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const url = request.url?.includes('_next') ? null : request.url;
const sp = url?.split?.('?')?.[1] || '';
const response = NextResponse.next();
if (url !== null) {
response.headers.set('searchParams', sp);
}
return response;
}
// 目标布局组件
import { headers } from 'next/headers';
import { decodeState } from 'state-in-url/encodeState';
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const sp = headers().get('searchParams') || '';
return (
<div>
<Comp1 searchParams={decodeState(sp, stateShape)} />
{children}
</div>
);
}
使用任意状态形状(不推荐)
'use client'
import { useUrlState } from 'state-in-url/next';
const someObj = {};
function SettingsComponent() {
const { urlState, setUrl, setState } = useUrlState<object>(someObj);
}用于 Remix.js 的 useUrlState hook
API 与 Next.js 版本相同,但可以传递 NavigateOptions 类型的选项。
示例
export const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: { id: string; value: { text: string; time: Date } }[];
};
import { useUrlState } from 'state-in-url/remix';
import { form } from './form';
function TagsComponent() {
const { urlState, setUrl, setState } = useUrlState(form);
const onChangeTags = React.useCallback(
(tag: (typeof tags)[number]) => {
setUrl((curr) => ({
...curr,
tags: curr.tags.find((t) => t.id === tag.id)
? curr.tags.filter((t) => t.id !== tag.id)
: curr.tags.concat(tag),
}));
},
[setUrl],
);
return (
<div>
<Field text="Tags">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
</div>
</Field>
<input value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// 可以立即更新状态,但根据需要将更改同步到 url
onBlur={() => setUrl()}
/>
</div>
);
}
const tags = [
{
id: '1',
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
},
{
id: '2',
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
},
{
id: '3',
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
},
];用于 React-Router 的 useUrlState hook
API 与 Next.js 版本相同,但可以传递 NavigateOptions 类型的选项。
示例
export const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
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';
// 对于 react-router v6
// import { useUrlState } from 'state-in-url/react-router6';
import { form } from './form';
function TagsComponent() {
const { urlState, setUrl, setState } = useUrlState(form);
const onChangeTags = React.useCallback(
(tag: (typeof tags)[number]) => {
setUrl((curr) => ({
...curr,
tags: curr.tags.find((t) => t.id === tag.id)
? curr.tags.filter((t) => t.id !== tag.id)
: curr.tags.concat(tag),
}));
},
[setUrl],
);
return (
<div>
<Field text="Tags">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
</div>
</Field>
<input value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// 可以立即更新状态,但根据需要将更改同步到 url
onBlur={() => setUrl()}
/>
</div>
);
}
const tags = [
{
id: '1',
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
},
{
id: '2',
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
},
{
id: '3',
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
},
];用于 React 的 useUrlState hook(无路由器)
适用于没有路由器的 React 应用 —— Vite、Create React App,或挂载到你无法控制的页面中的小部件。该 hook 通过 window.history 写入 URL,并在前进/后退以及任何其他 pushState/replaceState 时读回。使用同一个 state 对象的每个组件都共享该状态,既不需要包裹组件,也不需要通过 props 传递。
示例
// useFilters.ts
import { useUrlState } from 'state-in-url/react';
type FiltersState = { sort: 'name' | 'date'; page: number };
const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
export function useFilters() {
return useUrlState(FILTERS_STATE);
}// FiltersBar.tsx
import { useFilters } from './useFilters';
export function FiltersBar() {
const { urlState, setUrl } = useFilters();
return <button onClick={() => setUrl({ page: urlState.page + 1 })}>Page {urlState.page}</button>;
}state-in-url/astro 就是这个 hook;之所以在下文单独说明,是因为 islands 还会把 searchParams 作为 prop 传入。
用于 Astro 的 useUrlState hook
适用于 React 岛屿(islands)。Astro 默认没有客户端路由器,因此该 hook 通过 window.history 写入 URL,并在浏览器前进/后退以及任何其他 pushState/replaceState 调用时将其读回,Astro 自带的 <ClientRouter /> 也包括在内。同一页面上的岛屿共享状态,无需用任何东西将它们包裹起来。
示例
// src/state.ts
export const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: { id: string; value: { text: string; time: Date } }[];
};页面必须按需渲染(output: 'server',或在页面上声明 export const prerender = false,并配置适配器):预渲染的页面没有请求,因此岛屿拿到的是 {},并在 hydration 之后再读取 URL。
---
// src/pages/index.astro
import { Form } from '../components/Form';
import { Status } from '../components/Status';
// 服务端渲染结果与 URL 一致,因此 hydration 无需纠正任何内容。
// 须为普通对象:岛屿的 props 会被序列化,URLSearchParams 则不会。
const searchParams = Object.fromEntries(Astro.url.searchParams);
---
<Form client:load searchParams={searchParams} />
<Status client:load searchParams={searchParams} />// src/components/Form.tsx
import React from 'react';
import { useUrlState } from 'state-in-url/astro';
import { form } from '../state';
export function Form({ searchParams }: { searchParams?: Record<string, string> }) {
const { urlState, setUrl, setState } = useUrlState(form, { searchParams });
const onChangeTags = React.useCallback(
(tag: (typeof tags)[number]) => {
setUrl((curr) => ({
...curr,
tags: curr.tags.find((t) => t.id === tag.id)
? curr.tags.filter((t) => t.id !== tag.id)
: curr.tags.concat(tag),
}));
},
[setUrl],
);
return (
<div>
{tags.map((tag) => (
<Tag
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
<input value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
// 可以立即更新状态,但根据需要将更改同步到 url
onBlur={() => setUrl()}
/>
</div>
);
}
const tags = [
{ id: '1', value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') } },
{ id: '2', value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') } },
{ id: '3', value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') } },
];
// Status.tsx,第二个岛屿,读取同一份状态
export function Status({ searchParams }: { searchParams?: Record<string, string> }) {
const { urlState } = useUrlState(form, { searchParams });
return <pre>{JSON.stringify(urlState, null, 2)}</pre>;
}Preact 岛屿的用法相同:使用 @astrojs/preact 并设置 compat: true 后,react 在服务端和客户端构建中都会解析为 preact/compat,上面的导入语句无需改动。
不使用岛屿时,即页面上完全没有客户端框架,同一份状态可通过 decodeState 和 encodeState 放在 frontmatter 中:
---
import { decodeState, encodeState } from 'state-in-url/encodeState';
import { form } from '../state';
const state = decodeState(Astro.url.searchParams, form);
const withName = encodeState({ ...state, name: 'Alice' }, form, Astro.url.searchParams);
---
<pre>{JSON.stringify(state)}</pre>
<a href={`?${withName}`}>Alice</a>实用技巧
自定义 hook 以便捷地处理状态切片
'use client';
import React from 'react';
import { useUrlState } from 'state-in-url/next';
const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: {id: string; value: {text: string; time: Date } }[];
};
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
searchParams,
});
// 第一次导航将推送新的历史记录条目
// 所有后续操作将仅替换该条目
// 这样历史记录中只有 2 个条目 - ['/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 };
};使用复杂状态形状
export const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: { id: string; value: { text: string; time: Date } }[];
};'use client'
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
function TagsComponent() {
// `urlState` 将从 Form 类型推断!
const { urlState, setUrl } = useUrlState(form);
const onChangeTags = React.useCallback(
(tag: (typeof tags)[number]) => {
setUrl((curr) => ({
...curr,
tags: curr.tags.find((t) => t.id === tag.id)
? curr.tags.filter((t) => t.id !== tag.id)
: curr.tags.concat(tag),
}));
},
[setUrl],
);
return (
<div>
<Field text="Tags">
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<Tag
active={!!urlState.tags.find((t) => t.id === tag.id)}
text={tag.value.text}
onClick={() => onChangeTags(tag)}
key={tag.id}
/>
))}
</div>
</Field>
</div>
);
}
const tags = [
{
id: '1',
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
},
{
id: '2',
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
},
{
id: '3',
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
},
];仅更新状态并手动同步到 URL
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
React.useEffect(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
// 将按内容而不是引用比较状态,仅对新值触发更新
setUrl(urlState);
}, 500);
return () => {
clearTimeout(timer.current);
};
}, [urlState, setUrl]);在 onBlur 时同步状态将更符合实际使用情况。
<input onBlur={() => updateUrl()} .../>其他 hooks 和辅助函数
用于其他路由器的 useUrlStateBase hook
用它为本包未提供入口的路由器(例如 TanStack Router)构建自己的 useUrlState。传入任意带有 push(url) 和 replace(url) 的记忆化对象即可。
如果完全没有路由器,则不需要它 —— 请使用 state-in-url/react,它就是已经接好 window.history 的这个 hook。
import { useUrlStateBase } from 'state-in-url/useUrlStateBase';
type FiltersState = { sort: 'name' | 'date'; page: number };
const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
export function useFilters() {
const router = useYourRouter();
const nav = React.useMemo(
() => ({
push: (url: string) => router.navigate(url),
replace: (url: string) => router.navigate(url, { replace: true }),
}),
[router],
);
return useUrlStateBase(FILTERS_STATE, nav, ({ parse }) => parse(window.location.search));
}与 useUrlState 有两点不同:
- 它返回
state、updateUrl、updateState,而不是urlState、setUrl、setState;并且updateUrl默认为 push,而setUrl默认为 replace。 - 第三个参数用于填充初始 state。省略它,hook 就会忽略页面加载时 URL 中的查询字符串,于是分享出去的链接打开后只显示默认值。
[!NOTE]
useUrlState不再从包的根入口导出 —— 每个useUrlState都与某个框架绑定,因此各自位于独立入口(state-in-url/next、/react、/react-router、/react-router6、/remix、/astro)。根入口只保留与框架无关的部分:useSharedState、useUrlEncode、useUrlStateBase、useLinkProps、encode/decode和encodeState/decodeState。
useSharedState hook 用于 React.js
在任何 React 组件之间共享状态的 Hook,已在 Next.js 和 Vite 中测试。
'use client'
import { useSharedState } from 'state-in-url';
export const someState = { name: '' };
function SettingsComponent() {
const { state, setState } = useSharedState(someState);
}useLinkProps hook 用于 React.js
用于把状态带到指向另一个路由的链接上的 hook,例如语言切换器。setUrl 始终写入当前路径,这个 hook 不会。
'use client'
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useLinkProps } from 'state-in-url/useLinkProps';
export const form = { name: '' };
function LanguagePicker() {
const linkProps = useLinkProps(form, useRouter().push);
return <Link {...linkProps('/de/pricing')}>Deutsch</Link>;
}标记中保留原始的 href,因此爬虫和 hreflang 看到的是规范 URL;状态在点击时读取。
useUrlEncode hook 用于 React.js
encodeState 和 decodeState 辅助函数
encode 和 decode 辅助函数
最佳实践
- 将状态形状定义为常量
- 使用 TypeScript 以增强类型安全和自动完成
- 避免在 URL 参数中存储敏感信息(SSN、API 密钥等)
- 使用此扩展以获得可读的 TS 错误
可以为状态切片创建状态 hooks,并在整个应用程序中重用它们。例如:
type UserState = {
name: string;
age: number;
other: { id: string, value: number }[]
};
const userState = {
name: '',
age: 0,
other: [],
};
export const useUserState = () => {
const { urlState, setUrl, reset } = useUrlState(userState);
// 其他逻辑
// 导航到其他页面时重置查询参数
React.useEffect(() => {
return reset
}, [])
return { userState: urlState, setUserState: setUrl };;
}
注意事项
只能传递可序列化的值,
Function、BigInt或Symbol不起作用,可能像ArrayBuffer这样的东西也不行。所有可以序列化为 JSON 的内容都可以工作。Vercel 服务器将标头大小(查询字符串和其他内容)限制为 14KB,因此请将 URL 状态保持在约 5000 个字以下。https://vercel.com/docs/errors/URL_TOO_LONG
已在带有 app router 的
next.js14/15/16 中测试,没有计划支持 pages。Vitest + react-router 7:如果某个测试在
<BrowserRouter>内渲染组件时抛出useNavigate() may be used only in the context of a <Router> component,而 provider 明明就在那里,那么要么升级到react-router@8,要么把本包内联:// vitest.config.ts test: { server: { deps: { inline: ['state-in-url'] } } }Vitest 会把
node_modules外部化,于是state-in-url由 Node 加载,而你的测试文件由 Vite 加载。在react-router@7中,node导出条件把module/module-sync指向index.mjs,把default指向index.js,两个解析器各选其一——于是 react-router 被加载了两次,它的 React context 也存在两份,provider 写入的是 hook 读不到的那一份。内联能让双方都走 Vite 的解析器。react-router@8把default和module-sync指向同一个文件,因此任何解析器都不会产生分歧,也就不需要做什么。错误信息里从不出现state-in-url,所以这条说明放在这里。Jest:本包仅以 ESM 方式解析,而 Jest 默认以 CommonJS 运行测试,因此直接
require()会失败并报Must use import to load ES Module。Jest 不会继承 Node 已无需标志的require(esm)——它使用自己的同步vmAPI——所以在 Node 24.9+ 上要给它加上标志:// package.json "scripts": { "test": "NODE_OPTIONS=--experimental-vm-modules jest" }如果你不想加标志,
dist/里也附带了一份 CommonJS 构建产物。它被刻意排除在exports映射之外,因此不会有解析意外落到它上面——请显式地把 Jest 指向它:// jest.config.js moduleNameMapper: { '^state-in-url$': '<rootDir>/node_modules/state-in-url/dist/index.cjs', '^state-in-url/utils$': '<rootDir>/node_modules/state-in-url/dist/utils.cjs', '^state-in-url/(.*)$': '<rootDir>/node_modules/state-in-url/dist/$1/index.cjs', },这会在整个测试进程中替换为 CJS 构建产物,是安全的做法。不要在同一个进程里混用两者——本包把共享状态的存储放在模块作用域,两份副本就意味着两个存储,
useSharedState会悄无声息地停止共享。还有一点不同:
jest.mock()对 ES 模块无效。如果要 mock 本包,请使用jest.unstable_mockModule()。Vitest、Next.js、Vite、Astro 和 Remix 都不需要这些 Jest 配置。Vitest 唯一需要的额外步骤是上面的
server.deps.inline,而且仅在使用react-router@7时。
其他
贡献和/或本地运行
参见贡献文档
路线图
- [x] 用于
Next.js的 hook - [x] 用于
react-router的 hook - [x] 用于
remix的 hook - [ ] 用于
svelte的 hook - [x] 用于
astro的 hook - [ ] 用于在 hash 中存储状态的 hook ?
联系和支持
- 创建 GitHub issue 用于错误报告、功能请求或问题咨询
更新日志
提及
许可证
该项目根据 MIT 许可证授权。
招聘我
我是 Aleksandr Smyshliaev —— 本库唯一的作者和维护者。 资深前端工程师(React / Next.js / TypeScript,8 年以上),目前接受全职远程工作。
这个库是我所擅长之处的缩影:在混乱的浏览器原语之上提供类型化 API、零依赖,以及跨多个 React 大版本的 Next.js、Remix 和 React Router 稳定性。
- 最擅长 —— 组件库、状态管理,以及经得起重构的测试套件。
- 也是我的作品 —— react-horizontal-scrolling-menu (周安装量约 84k)、 test-proxy-recorder (Playwright 的录制/回放)、 llm-queue。
- 所在地 —— 格鲁吉亚第比利斯(GMT+4),与 CET 完全重叠。已注册为承包商实体,因此 B2B 合作无需雇主记录(employer-of-record)安排。
- 联系我 —— asmyshlyaev177.dev · [email protected] · LinkedIn · Telegram @asmyshlyaev177
