nuqs

nuqs

Next.js应用的URL查询状态管理工具

nuqs是一个为Next.js设计的URL查询状态管理工具。它提供useQueryState钩子,将状态存储在URL查询字符串中,支持app和pages路由器。nuqs具有内置解析器、历史管理和浅层更新等功能,简化了Next.js应用中的URL状态管理过程。

Next.jsuseQueryStateURL查询参数React Hook状态管理Github开源项目

Next.js 的 useQueryState

NPM MIT 许可证 持续集成 Depfu

Next.js 的 useQueryState 钩子 - 类似 React.useState,但存储在 URL 查询字符串中

特性

  • 🔀 同时支持 apppages 路由器
  • 🧘‍♀️ 简单:URL 是唯一真实来源
  • 🕰 替换历史记录或追加以使用后退按钮导航状态更新
  • ⚡️ 内置常见状态类型的解析器(整数、浮点数、布尔值、日期等)
  • ♊️ 使用 useQueryStates 关联查询字符串
  • 📡 URL 查询更新默认使用浅层模式,可选择通知服务器组件
  • 🗃 新功能:服务器缓存,用于在嵌套服务器组件中类型安全地访问 searchParams
  • ⌛️ **新功能:**支持 useTransition 以获取服务器更新的加载状态

安装

pnpm add nuqs
yarn add nuqs
npm install nuqs

注意:包正在更名为:nuqs :tada:

1.x 版本也将在 next-usequerystate 下可用, 但 2.x 及以后版本将只在 nuqs 下发布。

我应该使用哪个版本?

Next.js 版本范围支持的 nuqs / next-usequerystate 版本
>=14.0.4nuqs@latest
14.0.3nuqs@latest,需启用 windowHistorySupport 实验性标志,详见 #417
14.0.2不兼容,详见问题 #388 和 Next.js PR #58297
>= 13.1 && <= 14.0.1nuqs@latest
< 13.1next-usequerystate@1.7.3

使用方法

'use client' // app 路由器:仅在客户端组件中有效 import { useQueryState } from 'nuqs' export default () => { const [name, setName] = useQueryState('name') return ( <> <h1>你好,{name || '匿名访客'}</h1> <input value={name || ''} onChange={e => setName(e.target.value)} /> <button onClick={() => setName(null)}>清除</button> </> ) }

文档

useQueryState 需要一个必要参数:在查询字符串中使用的键。

React.useState 类似,它返回一个数组,包含查询字符串中的值(字符串类型,如果未找到则为 null)和一个状态更新函数。

我们的 Hello World 示例的输出示例:

URLname 值备注
/nullURL 中没有 name
/?name=''空字符串
/?name=foo'foo'
/?name=2'2'默认总是返回字符串,参见下面的解析部分

解析

如果你的状态类型不是字符串,你必须在第二个参数对象中传递一个解析函数。

我们为常见和更高级的对象类型提供了解析器:

import { parseAsString, parseAsInteger, parseAsFloat, parseAsBoolean, parseAsTimestamp, parseAsIsoDateTime, parseAsArrayOf, parseAsJson, parseAsStringEnum, parseAsStringLiteral, parseAsNumberLiteral } from 'nuqs' useQueryState('tag') // 默认为字符串 useQueryState('count', parseAsInteger) useQueryState('brightness', parseAsFloat) useQueryState('darkMode', parseAsBoolean) useQueryState('after', parseAsTimestamp) // 状态为 Date 类型 useQueryState('date', parseAsIsoDateTime) // 状态为 Date 类型 useQueryState('array', parseAsArrayOf(parseAsInteger)) // 状态为 number[] 类型 useQueryState('json', parseAsJson<Point>()) // 状态为 Point 类型 // 枚举(仅限字符串类型) enum Direction { up = 'UP', down = 'DOWN', left = 'LEFT', right = 'RIGHT' } const [direction, setDirection] = useQueryState( 'direction', parseAsStringEnum<Direction>(Object.values(Direction)) // 传递允许的值列表 .withDefault(Direction.up) ) // 字面量(仅限字符串类型) const colors = ['red', 'green', 'blue'] as const const [color, setColor] = useQueryState( 'color', parseAsStringLiteral(colors) // 传递只读的允许值列表 .withDefault('red') ) // 字面量(仅限数字类型) const diceSides = [1, 2, 3, 4, 5, 6] as const const [side, setSide] = useQueryState( 'side', parseAsNumberLiteral(diceSides) // 传递只读的允许值列表 .withDefault(4) )

您可以传入自定义的 parseserialize 函数:

import { useQueryState } from 'nuqs' export default () => { const [hex, setHex] = useQueryState('hex', { // TypeScript 将根据 `parse` 返回的内容自动推断它是一个数字 parse: (query: string) => parseInt(query, 16), serialize: value => value.toString(16) }) }

在服务器组件中使用解析器

注意:关于在服务器组件中实现类型安全的更友好方式,请参阅在服务器组件中访问 searchParams 部分。

如果您希望在服务器组件中解析 searchParams,您需要从 nuqs/server 导入解析器,这个模块不包含 "use client" 指令。

然后您可以使用 parseServerSide 方法:

import { parseAsInteger } from 'nuqs/server' type PageProps = { searchParams: { counter?: string | string[] } } const counterParser = parseAsInteger.withDefault(1) export default function ServerPage({ searchParams }: PageProps) { const counter = counterParser.parseServerSide(searchParams.counter) console.log('服务器端计数器: %d', counter) return ( ... ) }

查看服务器端解析演示以获取一个展示如何在客户端和服务器代码之间重用解析器配置的实时示例。

注意:解析器不会验证您的数据。如果您期望正整数或特定形状的 JSON 编码对象,您需要将解析器的结果传递给模式验证库,比如 Zod

默认值

当 URL 中不存在查询字符串时,默认行为是返回 null 作为状态。

这可能会使状态更新和 UI 渲染变得繁琐。看看这个存储在 URL 中的简单计数器示例:

import { useQueryState, parseAsInteger } from 'nuqs' export default () => { const [count, setCount] = useQueryState('count', parseAsInteger) return ( <> <pre>计数: {count}</pre> <button onClick={() => setCount(0)}>重置</button> {/* 在 setCount 中处理 null 值很烦人: */} <button onClick={() => setCount(c => c ?? 0 + 1)}>+</button> <button onClick={() => setCount(c => c ?? 0 - 1)}>-</button> <button onClick={() => setCount(null)}>清除</button> </> ) }

您可以指定在这种情况下要返回的默认值:

const [count, setCount] = useQueryState('count', parseAsInteger.withDefault(0)) const increment = () => setCount(c => c + 1) // c 永远不会是 null const decrement = () => setCount(c => c - 1) // c 永远不会是 null const clearCount = () => setCount(null) // 从 URL 中移除查询

注意:默认值是 React 内部的,它不会被写入 URL。

将状态设置为 null 将从查询字符串中移除该键,并将状态设置为默认值。

选项

历史记录

默认情况下,状态更新是通过用更新后的查询替换当前历史记录条目来完成的。

您可以将此视为 git squash 的一种形式,其中所有更改状态的操作都合并到单个历史记录值中。

您也可以选择为每个状态更改推送一个新的历史记录项,按键进行,这将让您使用后退按钮来导航状态更新:

// 默认:用新状态替换当前历史记录 useQueryState('foo', { history: 'replace' }) // 将状态更改追加到历史记录: useQueryState('foo', { history: 'push' })

history 选项的任何其他值都会回退到默认值。

您也可以在调用状态更新函数时覆盖历史记录模式:

const [query, setQuery] = useQueryState('q', { history: 'push' }) // 这会覆盖钩子声明设置: setQuery(null, { history: 'replace' })

浅更新

默认情况下,查询状态更新以 客户端优先 的方式进行:不会向服务器发出网络调用。

这相当于将 Next.js pages 路由器的 shallow 选项设置为 true,或在 app 路由器中使用实验性的 windowHistorySupport 标志。

要选择让查询更新通知服务器(在 pages 路由器中重新运行 getServerSideProps,在 app 路由器中重新渲染服务器组件),您可以将 shallow 设置为 false

const [state, setState] = useQueryState('foo', { shallow: false }) // 您也可以在调用 setState 时传递选项: setState('bar', { shallow: false })

滚动

Next.js 路由器在导航更新时会滚动到页面顶部,这在使用本地状态更新查询字符串时可能不太理想。

默认情况下,查询状态更新不会滚动到页面顶部,但您可以选择启用此行为(这在 1.8.0 版本之前是默认行为):

const [state, setState] = useQueryState('foo', { scroll: true }) // 您也可以在调用 setState 时传递选项: setState('bar', { scroll: true })

限制 URL 更新频率

由于浏览器对历史 API 的速率限制,内部对 URL 的更新会被排队并限制在默认的 50ms 内,这似乎能满足大多数浏览器的需求,即使在发送高频率的查询更新时,比如绑定到文本输入或滑块。

Safari 的速率限制要高得多,需要大约 340ms 的限制。如果您最终需要更长的更新间隔时间,可以在选项中指定:

useQueryState('foo', { // 最多每秒向服务器发送一次更新 shallow: false, throttleMs: 1000 }) // 您也可以在调用 setState 时传递选项: setState('bar', { throttleMs: 1000 })

注意:钩子返回的状态始终会立即更新,以保持 UI 的响应性。只有对 URL 的更改和使用 shallow: false 时的服务器请求会被限制频率。

如果多个钩子在同一个事件循环tick中设置了不同的限制值,将使用最高的值。此外,低于 50ms 的值将被忽略,以避免速率限制问题。了解更多

过渡

当与 shallow: false 结合使用时,您可以使用 useTransition 钩子来获取加载状态,以在服务器使用更新后的 URL 重新渲染服务器组件时显示。 将 useTransitionstartTransition 函数传入选项中以启用此行为(这会自动为你设置 shallow: false):

'use client' import React from 'react' import { useQueryState, parseAsString } from 'nuqs' function ClientComponent({ data }) { // 1. 提供你自己的 useTransition 钩子: const [isLoading, startTransition] = React.useTransition() const [query, setQuery] = useQueryState( 'query', // 2. 将 `startTransition` 作为选项传入: parseAsString().withOptions({ startTransition }) ) // 3. 当通过 `setQuery` 更新查询时, // 服务器重新渲染并流式传输 RSC 负载期间,`isLoading` 将为 true。 // 显示加载状态 if (isLoading) return <div>加载中...</div> // 使用数据进行正常渲染 return <div>{/*...*/}</div> }

配置解析器、默认值和选项

你可以使用构建器模式来方便地指定所有这些内容:

useQueryState( 'counter', parseAsInteger.withDefault(0).withOptions({ history: 'push', shallow: false }) )

你也可以为自定义解析器获得这种模式,并与其他解析器组合:

import { createParser, parseAsHex } from 'nuqs' // 将你的解析器/序列化器包装在 `createParser` 中 // 可以使其获得构建器模式和服务器端解析能力: const hexColorSchema = createParser({ parse(query) { if (query.length !== 6) { return null // 对于无效输入始终返回 null } return { // 当组合其他解析器时,它们也可能返回 null。 r: parseAsHex.parse(query.slice(0, 2)) ?? 0x00, g: parseAsHex.parse(query.slice(2, 4)) ?? 0x00, b: parseAsHex.parse(query.slice(4)) ?? 0x00 } }, serialize({ r, g, b }) { return ( parseAsHex.serialize(r) + parseAsHex.serialize(g) + parseAsHex.serialize(b) ) } }) // 例如:直接设置常用选项 .withOptions({ history: 'push' }) // 或在使用时设置: useQueryState( 'tribute', hexColorSchema.withDefault({ r: 0x66, g: 0x33, b: 0x99 }) )

注意:可以在 hex-colors 演示 中查看此示例的运行情况。

多个查询(批处理)

你可以在单个事件循环中调用多个状态更新函数,它们将异步应用到 URL:

const MultipleQueriesDemo = () => { const [lat, setLat] = useQueryState('lat', parseAsFloat) const [lng, setLng] = useQueryState('lng', parseAsFloat) const randomCoordinates = React.useCallback(() => { setLat(Math.random() * 180 - 90) setLng(Math.random() * 360 - 180) }, []) }

如果你想知道 URL 何时更新以及包含什么内容,可以等待状态更新函数返回的 Promise,它会给你更新后的 URLSearchParameters 对象:

const randomCoordinates = React.useCallback(() => { setLat(42) return setLng(12) }, []) randomCoordinates().then((search: URLSearchParams) => { search.get('lat') // 42 search.get('lng') // 12,已被排队并批量更新 })
<details> <summary><em>实现细节(Promise 缓存)</em></summary>

返回的 Promise 会被缓存,直到下一次刷新 URL 发生。因此,在同一事件循环中对任何钩子的 setState 的所有调用都将返回相同的 Promise 引用。

由于对 Web History API 的调用进行了节流,Promise 可能会被缓存多个事件循环。批处理更新将被合并并一次性刷新到 URL。这意味着如果在刷新发生之前有另一个更新覆盖了它,并非每个 setState 都会反映到 URL 上。

返回的 React 状态会立即反映所有设置的值,以保持 UI 的响应性。


</details>

useQueryStates

对于应该始终一起移动的查询键,你可以使用 useQueryStates 和一个包含每个键类型的对象:

import { useQueryStates, parseAsFloat } from 'nuqs' const [coordinates, setCoordinates] = useQueryStates( { lat: parseAsFloat.withDefault(45.18), lng: parseAsFloat.withDefault(5.72) }, { history: 'push' } ) const { lat, lng } = coordinates // 一次性设置所有(或部分)键: const search = await setCoordinates({ lat: Math.random() * 180 - 90, lng: Math.random() * 360 - 180 })

在服务器组件中访问 searchParams

如果你想在深层嵌套的服务器组件中访问 searchParams(即不在 Page 组件中),你可以使用 createSearchParamsCache 以类型安全的方式实现。

注意:解析器不验证你的数据。如果你期望正整数或特定形状的 JSON 编码对象,你需要将解析器的结果传递给模式验证库,如 Zod

// searchParams.ts import { createSearchParamsCache, parseAsInteger, parseAsString } from 'nuqs/server' // 注意:从 'nuqs/server' 导入以避免 "use client" 指令 export const searchParamsCache = createSearchParamsCache({ // 在这里列出你的搜索参数键和相关的解析器: q: parseAsString.withDefault(''), maxResults: parseAsInteger.withDefault(10) }) // page.tsx import { searchParamsCache } from './searchParams' export default function Page({ searchParams }: { searchParams: Record<string, string | string[] | undefined> }) { // ⚠️ 不要忘记在这里调用 `parse`。 // 你可以从返回的对象中访问类型安全的值: const { q: query } = searchParamsCache.parse(searchParams) return ( <div> <h1>搜索结果:{query}</h1> <Results /> </div> ) } function Results() { // 在子服务器组件中访问类型安全的搜索参数: const maxResults = searchParamsCache.get('maxResults') return <span>显示最多 {maxResults} 个结果</span> }

缓存仅对当前页面渲染有效(参见 React 的 cache 函数)。

注意:缓存仅适用于服务器组件,但你可以与 useQueryStates 共享解析器声明,以在客户端组件中实现类型安全:

// searchParams.ts import { parseAsFloat, createSearchParamsCache } from 'nuqs/server' export const coordinatesParsers = { lat: parseAsFloat.withDefault(45.18), lng: parseAsFloat.withDefault(5.72) } export const coordinatesCache = createSearchParamsCache(coordinatesParsers) // page.tsx import { coordinatesCache } from './searchParams' import { Server } from './server' import { Client } from './client' export default function Page({ searchParams }) { coordinatesCache.parse(searchParams) return ( <> <Server /> <Suspense> <Client /> </Suspense> </> ) } // server.tsx import { coordinatesCache } from './searchParams' export function Server() { const { lat, lng } = coordinatesCache.all() // 或单独访问键: const lat = coordinatesCache.get('lat') const lng = coordinatesCache.get('lng') return ( <span> 纬度: {lat} - 经度: {lng} </span> ) } // client.tsx // prettier-ignore ;'use client' import { useQueryStates } from 'nuqs' import { coordinatesParsers } from './searchParams' export function Client() { const [{ lat, lng }, setCoordinates] = useQueryStates(coordinatesParsers) // ... }

序列化器辅助函数

为了用状态值填充 <Link> 组件,你可以使用 createSerializer 辅助函数。

将描述搜索参数的对象传递给它,它会给你一个函数,你可以用值调用该函数,生成一个像钩子那样序列化的查询字符串。

示例:

import { createSerializer, parseAsInteger, parseAsIsoDateTime, parseAsString, parseAsStringLiteral } from 'nuqs/server' const searchParams = { search: parseAsString, limit: parseAsInteger, from: parseAsIsoDateTime, to: parseAsIsoDateTime, sortBy: parseAsStringLiteral(['asc', 'desc'] as const) } // 通过传递要接受的搜索参数的描述来创建一个序列化函数 const serialize = createSerializer(searchParams) // 然后,传递一些值(子集)并将它们渲染成查询字符串 serialize({ search: 'foo bar', limit: 10, from: new Date('2024-01-01'), // 这里我们省略 `to`,它不会被添加 sortBy: null // null 值也不会被渲染 }) // ?search=foo+bar&limit=10&from=2024-01-01T00:00:00.000Z

基础参数

返回的 serialize 函数可以接受一个基础参数,在此基础上附加/修改搜索参数:

serialize('/path?baz=qux', { foo: 'bar' }) // /path?baz=qux&foo=bar const search = new URLSearchParams('?baz=qux') serialize(search, { foo: 'bar' }) // ?baz=qux&foo=bar const url = new URL('https://example.com/path?baz=qux') serialize(url, { foo: 'bar' }) // https://example.com/path?baz=qux&foo=bar // 传递 null 会删除现有值 serialize('?remove=me', { foo: 'bar', remove: null }) // ?foo=bar

测试

目前,测试使用 useQueryState(s) 的组件行为的最佳方式是端到端测试,使用像 Playwright 或 Cypress 这样的工具。

在隔离环境中运行使用 Next.js 路由器的组件需要模拟它,这正在为应用路由器开发中

有关更多与测试相关的讨论,请参见 issue #259。

调试

你可以通过在浏览器中将 localStorage 中的 debug 项设置为 nuqs,并重新加载页面来启用调试日志。

// 在你的开发者工具中: localStorage.setItem('debug', 'nuqs')

注意:与 debug 包不同,这不支持通配符,但你可以组合使用:localStorage.setItem('debug', '*,nuqs')

日志行将以 [nuqs] 作为 useQueryState 的前缀,以 [nuq+] 作为 useQueryStates 的前缀,同时还有其他内部调试日志。

用户计时标记也会被记录,用于使用浏览器的开发者工具进行高级性能分析。

在提交issue时提供调试日志总是受欢迎的。🙏

注意事项

由于 Next.js 页面路由器在 SSR 上下文中不可用,这个钩子在 SSR/SSG 上总是返回 null(或提供的默认值)。

这个限制不适用于应用路由器。

SEO

如果你的页面使用查询字符串作为本地状态,你应该为你的页面添加一个规范 URL,以告诉 SEO 爬虫忽略查询字符串并索引没有它的页面。

在应用路由器中,这是通过元数据对象完成的:

import type { Metadata } from 'next' export const metadata: Metadata = { alternates: { canonical: '/url/path/without/querystring' } }

然而,如果查询字符串定义了页面显示的内容(例如:YouTube 的观看 URL,如 https://www.youtube.com/watch?v=dQw4w9WgXcQ),你的规范 URL 应该包含相关的查询字符串,你仍然可以使用 useQueryState 来读取它:

// page.tsx import type { Metadata, ResolvingMetadata } from 'next' import { useQueryState } from 'nuqs' import { parseAsString } from 'nuqs/server' type Props = { searchParams: { [key: string]: string | string[] | undefined } } export async function generateMetadata({ searchParams }: Props): Promise<Metadata> { const videoId = parseAsString.parseServerSide(searchParams.v) return { alternates: { canonical: `/watch?v=${videoId}` } } }

有损序列化

如果你的序列化器损失精度或不能准确表示底层状态值,当重新加载页面或从 URL 恢复状态(例如:在导航时)时,你将失去这种精度。

示例:

const geoCoordParser = { parse: parseFloat, serialize: v => v.toFixed(4) // 损失精度 } const [lat, setLat] = useQueryState('lat', geoCoordParser)

在这里,设置纬度为 1.23456789 将渲染 URL 查询字符串为 lat=1.2345,而内部 lat 状态将被正确设置为 1.23456789。

重新加载页面后,状态将被错误地设置为 1.2345。

许可证

MIT

François Best 用 ❤️ 制作 在工作中使用这个软件包吗?赞助我以帮助支持和维护。

项目分析和统计

编辑推荐精选

Keevx

Keevx

AI数字人视频创作平台

Keevx 一款开箱即用的AI数字人视频创作平台,广泛适用于电商广告、企业培训与社媒宣传,让全球企业与个人创作者无需拍摄剪辑,就能快速生成多语言、高质量的专业视频。

即梦AI

即梦AI

一站式AI创作平台

提供 AI 驱动的图片、视频生成及数字人等功能,助力创意创作

扣子-AI办公

扣子-AI办公

AI办公助手,复杂任务高效处理

AI办公助手,复杂任务高效处理。办公效率低?扣子空间AI助手支持播客生成、PPT制作、网页开发及报告写作,覆盖科研、商业、舆情等领域的专家Agent 7x24小时响应,生活工作无缝切换,提升50%效率!

TRAE编程

TRAE编程

AI辅助编程,代码自动修复

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
蛙蛙写作

蛙蛙写作

AI小说写作助手,一站式润色、改写、扩写

蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。

AI辅助写作AI工具蛙蛙写作AI写作工具学术助手办公助手营销助手AI助手
问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

热门AI助手AI对话AI工具聊天机器人
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

AI办公办公工具AI工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图热门
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

热门AI开发模型训练AI工具讯飞星火大模型智能问答内容创作多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

下拉加载更多