类型化状态,生活在 URL 中

useUrlState 是将自身写入查询字符串的 React 状态。对象、数组和日期保持其类型,每个状态都是一个可分享的链接,并在重新加载后仍然存在——无需 provider,也无需样板代码。

  • ~2 KB gzip 压缩
  • 零依赖
  • TypeScript 优先
  • Next.js / react-router / Remix
  • MIT
npm i state-in-url

useUrlState — 在线演示: next.js

在下方输入——观察 URL 亮起来

第一个客户端组件
另一个客户端组件

从 URL 读取——无需 props、无需 context,类型和结构得以保留

{
name: string
age: undefinedundefined
agree_to_terms: falseboolean
tags: []
}

相同的 API,三种路由器

快速开始

1. 定义状态
state
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 } }[];
};
2. 包成一个可复用的 hook
useFormState
'use client';

import { useUrlState } from 'state-in-url/next';
import { form } from './form';

// One hook per feature - the whole API for this state
// "searchParams" only needed to pass params from Server Components
export const useFormState = (searchParams?: object) =>
  useUrlState(form, { searchParams });
3. 在任意组件中使用——状态共享
ComponentA
'use client';

import { useFormState } from './useFormState';

export const ComponentA = () => {
  // see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
  const { urlState, setState, setUrl } = 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>
    </>
};
ComponentB
'use client';
import { useFormState } from './useFormState';

// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
  // same state as ComponentA - no props, no context
  const { urlState } = useFormState(searchParams);

// will be defaultValue from `form` if not in url, no need to check

  return <div>name: {urlState.name}</div>
};
4. 需要更多时再扩展 hook
useFormState - extended
'use client';

import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';

export const useFormState = ({ searchParams }: { searchParams?: object }) => {
  const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
    searchParams,
  });

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

正在使用 AI 编程代理?

代理在这里每次都犯同样的两个错误。它们用 interface 来定义状态形状,而 JSONCompatible 约束会直接拒绝它。而且它们在组件内部构建默认状态对象,这会悄悄破坏共享——它是按对象身份做键的,所以不会报错,两个组件只是不再能看到彼此。

所以这个包随附六个 SKILL.md 文件。你的代理通过 TanStack Intent 按需加载其中一个,并且它们与库一起版本化,而不是与本页面一起。

npx @tanstack/intent@latest install

在已经安装 state-in-url 的项目中运行一次。之后你的代理会在 node_modules/state-in-url/skills/ 中找到这些技能。

  • feature-state-hook定义状态,并将 useUrlState 封装在功能作用域的 hook 中
  • input-handling文本输入、滑块,以及任何变化很快的东西
  • nextjs-ssrApp Router:searchParams 转发、用于布局的 Proxy
  • react-router-remix-setupReact Router v6/v7 或 Remix v2 的设置
  • form-library-integration与 react-hook-form(或 formik)配合使用
  • shared-state-no-urluseSharedState — 不接触 URL 的共享

源文件在 GitHub 上. 无法加载 Intent 技能的代理应改读 llms.txt —— 相同的规则,浓缩在一个文件里。

state-in-url vs nuqs

在找 nuqs 的替代品?两者都把带类型的状态存进查询字符串;区别在于需要多少配置,以及值可以是什么。

对比项state-in-urlnuqs
配置无需配置——导入 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 会带来很大帮助。

分享给其他开发者

Uneed Embed Badge