state-in-url
是一个简单的状态管理工具,可以与URL同步。在不相关的React组件之间共享复杂状态,将状态同步到URL,对TS友好,兼容NextJS。
- 🙃 在不改变URL的情况下,在不同组件之间共享状态,是信号和其他状态管理工具的良好替代方案
- 🔗 包含完整应用程序状态的可共享URL
- 🔄 在页面重新加载之间轻松持久化状态
- 🧠 在不相关的客户端组件之间同步数据
- 🧮 在URL中存储未保存的用户表单
- 🧩 简单:无需提供者、reducers、样板代码或新概念
- 📘 TypeScript支持和类型安全:保留数据类型和结构,通过IDE建议、强类型和JSDoc注释增强开发体验
- ⚛️ 框架灵活性:为Next.js和React.js应用程序提供单独的钩子,以及用于纯JS的函数
- ⚙ 经过充分测试:单元测试和Playwright测试
- ⚡ 快速:最少的重新渲染
- 🪶 轻量级:零依赖,占用空间小
- 安装
- Next.js的
useUrlState
- React.js的
useSharedState
- React.js的
useUrlEncode
- 纯JS使用的
encodeState
和decodeState
- 自动同步状态到URL
- 低级
encode
和decode
函数 - 最佳实践
- 注意事项
- 联系与支持
- 更新日志
- 许可证
- 灵感来源
# npm
npm install --save state-in-url
# yarn
yarn add state-in-url
# pnpm
pnpm add state-in-url
在tsconfig.json
的compilerOptions
中设置"moduleResolution": "Node16"
或"moduleResolution": "NodeNext"
useUrlState
是一个为Next.js应用程序设计的自定义React钩子,使客户端组件之间的通信变得简单。它允许你共享任何复杂的状态并将其与URL搜索参数同步,提供了一种在页面重新加载之间持久化状态并通过URL共享应用程序状态的方法。
-
定义状态形状
// countState.ts // 状态形状应该存储在一个常量中,不要直接传递对象 export const countState: CountState = { count: 0 } type CountState = { count: number }
-
导入并使用
'use client'
import { useUrlState } from 'state-in-url/next';
import { countState } from './countState';
function MyComponent() {
// 用于从服务器组件使用searchParams
// 例如 export default async function Home({ searchParams }: { searchParams: object }) {
// const { state, updateState, updateUrl } = useUrlState(countState, searchParams);
const { state, updateState, updateUrl } = useUrlState(countState);
// 不允许你意外地直接修改状态,需要TS
// state.count = 2 // <- 错误
return (
<div>
<p>计数:{state.count}</p>
<button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
增加(更新URL)
</button>
// 与React.useState相同的API
<button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
增加(仅本地)
</button>
<button onClick={() => updateUrl()}>
同步更改到URL
// 或者不同步,只共享状态
</button>
<button onClick={() => updateUrl(state)}>
重置
</button>
</div>
)
}
interface UserSettings {
theme: 'light' | 'dark';
fontSize: number;
notifications: boolean;
}
export const userSettings: UserSettings {
theme: 'light',
fontSize: 16,
notifications: true,
}
'use client'
import { useUrlState } from 'state-in-url/next';
import { userSettings } from './userSettings';
function SettingsComponent() {
// `state` 将从UserSettings类型推断!
const { state, updateUrl } = useUrlState(userSettings);
const toggleTheme = () => {
updateUrl(current => ({
...current,
theme: current.theme === 'light' ? 'dark' : 'light',
}));
};
// 空闲时同步状态到URL
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
React.useEffect(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
// 将通过内容而不是引用比较状态,只为新值触发更新
updateUrl(state);
}, 500);
return () => {
clearTimeout(timer.current);
};
}, [state, updateUrl]);
return (
<div>
<h2>用户设置</h2>
<p>主题:{state.theme}</p>
<p>字体大小:{state.fontSize}px</p>
<button onClick={toggleTheme}>切换主题</button>
{/* 其他UI元素用于更新其他设置 */}
</div>
);
}
...
// 其他组件
function Component() {
const { state } = useUrlState(defaultSettings);
return (
<div>
<p>通知 {state.notifications ? '开启' : '关闭'}</p>
</div>
)
}
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
React.useEffect(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
// 将通过内容而不是引用比较状态,只为新值触发更新
updateUrl(state);
}, 500);
return () => {
clearTimeout(timer.current);
};
}, [state, updateUrl]);
export default async function Home({ searchParams }: { searchParams: object }) {
return (
<Form sp={searchParams} />
)
}
// Form.tsx
'use client'
import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
const Form = ({ sp }: { sp: object }) => {
const { state, updateState, updateUrl } = useUrlState(form, sp);
}
这是一个棘手的部分,因为使用app router的nextjs不允许从服务器端访问searchParams。有一个使用中间件的解决方法,但它并不漂亮,而且可能在nextjs更新后停止工作。
// 添加到适当的`layout.tsc`
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;
}
// 目标layout组件
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 { state, updateUrl, updateState } = useUrlState<object>(someObj);
}
用于在任何React组件之间共享状态的钩子,已在Next.js和Vite中测试。
'use client'
import { useSharedState } from 'state-in-url';
export const someState = { name: '' };
function SettingsComponent() {
const { state, setState } = useSharedState(someState);
}
- 将您的状态形状定义为常量,以确保一致性
- 使用TypeScript以增强类型安全性和自动完成功能
- 避免在URL参数中存储敏感信息
- 使用
updateState
进行频繁更新,使用updateUrl
同步更改到URL - 在Next.js中使用
Suspence
包装客户端组件 - 使用这个扩展以获得可读的TS错误
- 只能传递可序列化的值,
Function
、BigInt
或Symbol
不会工作,可能类似ArrayBuffer
的东西也不行。 - Vercel服务器限制头部(查询字符串和其他内容)的大小为14KB,所以保持您的URL状态在~5000个单词以下。https://vercel.com/docs/errors/URL_TOO_LONG
- 已在使用app router的
next.js
14中测试,不计划支持pages。
克隆此仓库,运行npm install
然后
npm run dev
- 创建GitHub issue报告bug、提出功能请求或提问
本项目采用MIT许可证。