Project Icon

nuqs

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

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

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,已被排队并批量更新
})
实现细节(Promise 缓存)

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

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

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


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 用 ❤️ 制作 在工作中使用这个软件包吗?赞助我以帮助支持和维护。

项目分析和统计

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号