面向 React 与 Next.js 的类型化 URL 状态—— 用法像 useState

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

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

useUrlState — 在线演示: next.js

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

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

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

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

Next.js、React Router、Remix 和 Astro 的 URL 状态管理——同一套 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 的设置
  • astro-setupAstro 岛屿(React 或 Preact),或没有客户端框架的页面
  • 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
配置Next.js、React Router v6/v7、Remix、Astro,纯 JS 辅助函数需要用适配器组件包裹应用
状态形态一个带类型的对象,用法类似 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。

阅读完整对比——同一个功能在两个库里的写法,以及如何迁移

React 中的 URL 状态——常见问题

为什么要把 React 状态放进 URL?
承载状态的 URL 就是一个可分享的链接:重新加载、收藏或发给别人,打开的都是同样的筛选条件、标签页或页码。后退和前进不用额外写代码,不相关的组件无需 provider 也能读到同一份值。state-in-url 用一个类型化对象做到这一点,而不是手动解析字符串。
什么样的状态适合放进 URL?
任何读者可能收藏或分享的东西:筛选条件、排序、分页、当前标签页、日期范围、搜索文本。私密的、体积巨大的或纯粹临时的不要放——认证令牌、对话框是否打开、鼠标位置。一个快速判断:带着这个值的分享链接还说得通吗?
如何用 state-in-url 在 React 中读取和设置 URL 参数?
用一个默认状态对象调用 useUrlState。urlState 保存当前值,已经带好类型;setUrl 把一个 partial 对象写进查询字符串;setState 只更新状态,在你主动写入 URL 之前不会触碰它。数字、布尔值、数组、嵌套对象和 Date 取回来时与存入时类型一致。
URL 状态在页面刷新后还在吗?
在。状态就是查询字符串本身,所以重新加载、收藏夹或粘贴到别处的链接都能恢复它。在 Next.js App Router 上,把页面的 searchParams prop 传给 hook,这样首次服务端渲染就已经显示正确的值,而不是默认值。
它能和 Next.js Server Components 一起用吗?不需要 Suspense 边界?
可以。该 hook 从不调用 useSearchParams,因此使用它的组件不需要 Suspense 边界,也不会让页面退出预渲染,PPR 也不例外。Server Components 通过 searchParams prop 读取同一份状态;布局则可以从 proxy.ts 里设置的请求头中解码它。
能把 react-hook-form 或表格库与 URL 同步吗?
能。让表单库继续做单一数据源,用 urlState 作为默认值初始化它,再在变更处理函数或 effect 里用 setUrl 把它的变化镜像到 URL。同样的模式适用于 TanStack Table 的状态、筛选面板,以及任何暴露出值和 setter 的东西。
state-in-url 支持哪些框架?
Next.js 14-16 App Router、React Router v6 和 v7、Remix v2,以及 Astro 岛屿(React 或 Preact),各有自己的入口。纯 JavaScript 和其他任何框架都可以直接使用 encodeState 和 decodeState 辅助函数。gzip 后 ~2 KB,零依赖。

为什么选择 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 指南

不用 Next.js 或 react-router?

这些 encodeState / decodeState 辅助函数可用于任何框架或纯 JS——hook 只是它们之上的一层便利封装。

来看看 GitHub 页面 —— 一个 star 会带来很大帮助。

分享给其他开发者

Uneed Embed Badge