nuqs@2.5.0 已发布,现在就试试吧:
npm install nuqs@latest这是一个重大发布,充满了期待已久的功能、bug 修复和改进,包括:
- ⏱️ Debounce:仅在用户停止在搜索输入框中输入时发送网络请求
- ☑️ Standard Schema:将验证和类型推断连接到外部工具(例如:tRPC)
- ⚡ Key isolation:仅在 URL 的对应部分发生变化时重新渲染组件
- 🏝️ TanStack Router 支持,带有类型安全的路由 (🧪 实验性)
Debounce
虽然 nuqs 一直以来都有一个节流系统,用于适应 浏览器对 URL 更新进行限速,
但这个系统对于高频输入来说并不理想,例如 <input type="search"> 或 <input type="range"> 滑块。
对于那些最终值才重要的场景,防抖比节流更有意义。
我需要防抖吗?
防抖仅适用于服务器端数据获取(RSC 和加载器,当与 shallow: false 结合使用时),
以控制何时向服务器发送请求。例如:它可以让你避免在搜索输入框中输入第一个字符时单独发送请求,而是等待用户输入完成。
钩子返回的状态始终立即更新:只有发送到服务器的网络请求会被防抖。
如果你是在客户端获取数据(例如:使用 TanStack Query),你将需要防抖返回的状态(使用第三方 useDebounce 实用钩子)。
你现在可以指定一个新选项 limitUrlUpdates,它替换了 throttleMs,并声明防抖或节流行为:
import { debounce, useQueryState } from 'nuqs'
function DebouncedSearchInput() {
// 在 250ms 不活动后向服务器发送更新
const [search, setSearch] = useQueryState('search', {
defaultValue: '',
shallow: false,
limitUrlUpdates: debounce(250)
})
// 你仍然可以使用受控组件:
// 本地状态会立即更新。
return (
<input
type="search"
value={search}
onChange={e => setSearch(e.target.value)}
/>
)
}阅读 完整文档 以了解 API、它的作用解释, 以及处理搜索输入时的提示列表 (你可能并不总是想要总是防抖)。
Standard Schema
你现在可以使用你的搜索参数定义对象
(那些你提供给
useQueryStates、
createLoader 和
createSerializer 的对象)来派生一个 Standard Schema 验证器,
你可以用它来进行基本的运行时验证和类型推断与其他工具集成,例如:
- tRPC,用于在将 URL 状态提供给它们时验证过程输入
- TanStack Router 的
validateSearch(见下文)
import {
createStandardSchemaV1,
parseAsInteger,
parseAsString,
} from 'nuqs'
// 1. 像往常一样定义你的搜索参数
export const searchParams = {
searchTerm: parseAsString.withDefault(''),
maxResults: parseAsInteger.withDefault(10)
}
// 2. 创建一个与 Standard Schema 兼容的验证器
export const validateSearchParams = createStandardSchemaV1(searchParams)
// 3. 将其与其他工具一起使用,例如 tRPC:
router({
search: publicProcedure.input(validateSearchParams).query(...)
})阅读 完整文档 以了解你可以传入的选项。
Key isolation
也称为 细粒度订阅,由 TanStack Router 率先采用,键隔离的想法是 监听 URL 中搜索参数键的组件应该仅在该键的值变化时重新渲染。
没有键隔离时,URL 的任何变化都会重新渲染所有监听它的组件。
看看这两个计数器按钮, 以及点击一个时如何重新渲染两者,没有键隔离 时:
而带有键隔离时会发生什么:
键隔离现在内置于以下适配器中:
- React SPA
- React Router (v6 & v7)
- Remix
- TanStack Router
没有 Next.js? 😢
不幸的是,Next.js 使用单个 Context 来携带 URLSearchParams 对象,每当任何搜索参数变化时,其引用都会变化,因此会重新渲染每个 useSearchParams 调用站点。
我正在与 Next.js 团队合作,寻找解决方案来改善每个人的性能(不仅仅是 nuqs 用户)。
TanStack Router
我们添加了对 TanStack Router 的实验性支持,因此你可以从 NPM 加载和使用 nuqs 启用的组件, 或在单体仓库中在不同框架之间共享。
TanStack Router 已经有了优秀的 API 用于类型安全的 URL 状态管理,我们鼓励你在应用代码中使用它们。这个适配器主要作为兼容层。
这还包括 有限 支持,将 nuqs 搜索参数定义连接到 TSR 的类型安全路由, 通过 Standard Schema 接口。
参考 完整文档 以了解支持的内容。
其他变更
选项的全局默认值
你现在可以在适配器级别为某些 选项 指定不同的默认值:
<NuqsAdapter
defaultOptions={{
shallow: false, // 更新时始终发送网络请求
scroll: true, // 更新时始终滚动到页面顶部
clearOnDefault: false, // 在 URL 中保留默认值
limitUrlUpdates: throttle(250), // 增加全局节流
}}
>
{children}
</NuqsAdapter>Next.js 15.5 typed routes 的预览支持
类型安全的路由现在在 Next.js 15.5 中作为选项可用。
虽然我仍在设计一个 API 来优雅地支持这个, 但对序列化器类型的少量更改可以让你在用户空间实验它,使用一个可复制粘贴的实用函数:
// 在你的代码库中复制这个
import { Route } from 'next'
import {
createSerializer,
type CreateSerializerOptions,
type ParserMap
} from 'nuqs/server'
export function createTypedLink<Parsers extends ParserMap>(
route: Route,
parsers: Parsers,
options: CreateSerializerOptions<Parsers> = {}
) {
const serialize = createSerializer<Parsers, Route, Route>(parsers, options)
return serialize.bind(null, route)
}用法:
import { createTypedLink } from '@/src/typed-links'
import { parseAsFloat, parseAsIsoDate, parseAsString, type UrlKeys } from 'nuqs'
// 重用你的搜索参数定义对象和 urlKeys:
const searchParams = {
latitude: parseAsFloat.withDefault(0),
longitude: parseAsFloat.withDefault(0),
}
const urlKeys: UrlKeys<typeof searchParams> = {
// 在 URL 中定义缩写
latitude: 'lat',
longitude: 'lng'
}
// 这是一个绑定到 /map 的函数,带有那些搜索参数和映射:
const getMapLink = createTypedLink('/map', searchParams, { urlKeys })
function MapLinks() {
return (
<Link
href={
getMapLink({ latitude: 48.86, longitude: 2.35 })
// → /map?lat=48.86&lng=2.35
}
>
Paris, France
</Link>
)
}这基于我在这段视频中在 React Router 的类型安全 href 实用工具中使用相同的技术:
我很快将开启一个 RFC 讨论来定义 API,目标是:
- 它应该同时支持 Next.js 和 React Router 的类型化路由 (如果我们也能连接到 TSR 那就太好了 👀)
- 它应该处理静态、动态和捕获所有路由,带有类型安全的路径名参数、搜索参数和哈希。
依赖和捆绑大小
nuqs 现在是一个零运行时依赖的库!🙌
虽然这个发布打包了很多新功能,但我们还是保持了 它在 5.5kB 以下(最小化 + 压缩)。
完整变更日志
功能
- #855Failed to fetch details: 401 Unauthorized
- #900Failed to fetch details: 401 Unauthorized
- #953Failed to fetch details: 401 Unauthorized
- #965Failed to fetch details: 401 Unauthorized
- #1038Failed to fetch details: 401 Unauthorized
- #1062Failed to fetch details: 401 Unauthorized
- #1066Failed to fetch details: 401 Unauthorized
- #1079Failed to fetch details: 401 Unauthorized
- #1083Failed to fetch details: 401 Unauthorized
Bug 修复
- #996Failed to fetch details: 401 Unauthorized
(有助于 ESM/CJS 互操作) - #1057Failed to fetch details: 401 Unauthorized
- #1063Failed to fetch details: 401 Unauthorized
- #1073Failed to fetch details: 401 Unauthorized
文档
- #787Failed to fetch details: 401 Unauthorized→ 阅读文档
- #976Failed to fetch details: 401 Unauthorized
- #1000Failed to fetch details: 401 Unauthorized
- #1004Failed to fetch details: 401 Unauthorized
- #1005Failed to fetch details: 401 Unauthorized
- #1017Failed to fetch details: 401 Unauthorized
- #1021Failed to fetch details: 401 Unauthorized
- #1025Failed to fetch details: 401 Unauthorized
- #1027Failed to fetch details: 401 Unauthorized
- #1032Failed to fetch details: 401 Unauthorized
- #1037Failed to fetch details: 401 Unauthorized
- #1041Failed to fetch details: 401 Unauthorized
- #1043Failed to fetch details: 401 Unauthorized
- #1046Failed to fetch details: 401 Unauthorized
- #1051Failed to fetch details: 401 Unauthorized
- #1052Failed to fetch details: 401 Unauthorized
- #1056Failed to fetch details: 401 Unauthorized
- #1058Failed to fetch details: 401 Unauthorized
- #1070Failed to fetch details: 401 Unauthorized
- #1082Failed to fetch details: 401 Unauthorized
其他变更
- #985Failed to fetch details: 401 Unauthorized
- #990Failed to fetch details: 401 Unauthorized
- #1011Failed to fetch details: 401 Unauthorized
- #1029Failed to fetch details: 401 Unauthorized
- #1033Failed to fetch details: 401 Unauthorized
- #1065Failed to fetch details: 401 Unauthorized
- #1067Failed to fetch details: 401 Unauthorized
- #1074Failed to fetch details: 401 Unauthorized
- #1077Failed to fetch details: 401 Unauthorized
- #1078Failed to fetch details: 401 Unauthorized
- #1080Failed to fetch details: 401 Unauthorized
- #1081Failed to fetch details: 401 Unauthorized
- #1086Failed to fetch details: 401 Unauthorized恭喜你的第一个 OSS 贡献!🙌
接下来是什么?
长期存在的问题和功能请求包括:
- 支持 原生数组,通过在 URL 中重复键(例如:
?foo=bar&foo=egg会给你['bar', 'egg']) - 使用 Standard Schema 的 运行时验证(Zod、Valibot、ArkType 等),以验证 TypeScript 无法表示的内容(例如数字范围和字符串格式)。
- 支持 Next.js 15.5 和 React Router 的
href实用工具中的 类型化链接。
感谢
我想感谢 赞助者、 贡献者 以及在 GitHub、Bluesky 和 X/Twitter 上提出问题、讨论和审查 PR 的人。 你们是推动这个项目前进的不断增长的社区, 我对响应感到非常高兴。
赞助者
感谢这些了不起的人和公司,我能够投入更多时间到这个项目中,让它变得更好,惠及每个人。 加入他们在 💖 GitHub Sponsors 上!
贡献者
巨大感谢 @87xie、@AfeefRazick、@ahmedrowaihi、@Amirmohammad-Bashiri、@AmruthPillai、@an-h2、@anhskohbo、@awosky、@brandanking-decently、@devhasson、@didemkkaslan、@dinogit、@dmytro-palaniichuk、@Elya29、@ericwang401、@ethanniser、@fuma-nama、@gensmusic、@I-3B、@jaberamin9、@Joehoel、@Kavan72、@krisnaw、@Manjit2003、@neefrehman、@phelma、@remcohaszing、@SeanCassiere、@snelsi、@stefan-schubert-sbb、@thewebartisan7、@TkDodo、@vanquishkuso 和 @Willem-Jaap 的帮助!