npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Readme

English · 简体中文 · 日本語 · 한국어 · Русский · Español · Português (BR) · Français · Tiếng Việt

State in url

Available for hire

npm npm bundle size (minified + gzip) Codacy Badge

Tests Codacy Badge Commitizen friendly semantic-release: angular

OpenSSF Scorecard OpenSSF Best Practices license

Demo-gif

演示

演示 | 镜像链接

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。

目录

安装

1. 安装包

# npm
npm install --save state-in-url
# yarn
yarn add state-in-url
# pnpm
pnpm add state-in-url

2. 编辑 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

完整 API 文档

React-Router 示例

使用示例

基础用法
  1. 使用默认值定义状态形状
// userState.ts
// 只有与默认值不同的参数才会进入 url。
export const userState: UserState = { name: '', age: 0 }

// 使用 `Type` 而不是 `Interface`!
type UserState = { name: string, age: number }
  1. 导入并使用
'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 类型的选项。

API 文档

示例

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 类型的选项。

API 文档

示例

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 传递。

API 文档

示例

// 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 /> 也包括在内。同一页面上的岛屿共享状态,无需用任何东西将它们包裹起来。

API 文档

示例

// 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>

示例代码,纯 Astro 页面

实用技巧

自定义 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。

API 文档

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);
}

API 文档

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;状态在点击时读取。

API 文档

useUrlEncode hook 用于 React.js

API 文档

encodeState 和 decodeState 辅助函数

API 文档

encode 和 decode 辅助函数

API 文档

最佳实践

  • 将状态形状定义为常量
  • 使用 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 };;
}

注意事项

  1. 只能传递可序列化的值,Function、BigInt 或 Symbol 不起作用,可能像 ArrayBuffer 这样的东西也不行。所有可以序列化为 JSON 的内容都可以工作。

  2. Vercel 服务器将标头大小(查询字符串和其他内容)限制为 14KB,因此请将 URL 状态保持在约 5000 个字以下。https://vercel.com/docs/errors/URL_TOO_LONG

  3. 已在带有 app router 的 next.js 14/15/16 中测试,没有计划支持 pages。

  4. 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,所以这条说明放在这里。

  5. Jest:本包仅以 ESM 方式解析,而 Jest 默认以 CommonJS 运行测试,因此直接 require() 会失败并报 Must use import to load ES Module。Jest 不会继承 Node 已无需标志的 require(esm)——它使用自己的同步 vm API——所以在 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 稳定性。

灵感来源

NUQS

在 Vue 中使用 URL 存储状态

在 URL 中存储状态

NextJS useSearchParams