クエリパラメータの同期を手軽に実装できる nuqs の紹介
こんにちは、steshima です。
クエリパラメータを state として扱い、簡単に URL と同期できる nuqs を紹介します。
執筆時の nuqs のバージョンは 2.7.2 です。
nuqs について
nuqs は URL のクエリパラメータをフックで扱えるようにするライブラリです。
React の useState と同じ感覚でクエリパラメータを読み書きでき、URL・ブラウザ履歴・React の状態を自動で同期してくれます。
nuqs 導入前の課題
ソーシャル PLUS では、テーブルのページネーションが存在する画面ではページ番号を URL のクエリパラメータで管理しています。
nuqs を導入する以前、そのクエリパラメータの取得・同期に Next Router を直接扱う下記のようなコードが各所に散見されていました。
import { useRouter } from 'next/router';
const router = useRouter();
const pageParam = router.query.page ?? '1';
const currentPage = parseInt(pageParam as string, 10) || 1;
ページ番号を取得する際は router.query から page パラメータを手動で取り出し、文字列から数値へパースしています。
page パラメータのデフォルト値や不正な値が設定されていた場合のフォールバックなども自前で書いており、なかなかの手間です。
また、ページ変更時は Next Router の push を使い、クエリにページ番号を付与して URL を同期させていました。
nuqs での書き換え
これを nuqs を使うことで下記のようにスッキリ書けます。(パッケージインストールなどは省略)
import { useQueryState, parseAsInteger, Options } from 'nuqs';
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1).withOptions({ history: 'push' }),
);
useQueryState を呼び出すだけで、クエリパラメータと React の状態を自動的に同期してくれます。
React の useState みたいで非常に直感的ですね。
クエリパラメータを書き換える場合は setPage を呼び出すだけです。
また withOptions で setPage 実行時にブラウザの履歴スタックに追加されるようにしています。
複数のクエリパラメータを管理したい場合は、useQueryState に加えて useQueryStates というフックも用意されています。
以下は公式ドキュメントの例です。
import { useQueryStates, parseAsFloat } from 'nuqs'
const [coordinates, setCoordinates] = useQueryStates(
{
lat: parseAsFloat.withDefault(45.18),
lng: parseAsFloat.withDefault(5.72)
},
{
history: 'push'
}
)
Built-in のパーサを活用する
nuqs には Built-in のパーサーが豊富に用意されています。
// Boolean
// ?bool=true / ?bool=false
useQueryState('bool', parseAsBoolean.withDefault(false));
// Array
// ?project=123&project=456 → [123, 456]
useQueryState('project', parseAsNativeArrayOf(parseAsInteger));
// ISO 8601 Date
// ?date=2025-11-18 → new Date('2025-11-18')
useQueryState('date', parseAsIsoDate.withDefault(new Date()));
便利だなと感じたのはリテラルのパーサーで、例えばタブを切り替える画面でクエリパラメータでタブを制御する場合、下記のように値を限定できます。
import { useQueryState, parseAsStringLiteral } from 'nuqs';
// `tab` の型は `"reserved" | "draft" | "completed" | "failed"` で推論される
const [tab] = useQueryState(
"status",
parseAsStringLiteral([
'reserved',
'draft',
'completed',
'failed',
]).withDefault("reserved"),
);
実務でのハマりどころ: Next Router × nuqs のテスト
ソーシャル PLUS では Next.js を使用していますが、テストで Next Router(Pages Router) 関連の問題がありました。
nuqs ではテスト用のアダプターが用意されており、それを使用することでモックなどせずに単体コンポーネントのテストができます。
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing';
const { result } = renderHook(() => useTheHookToTest(), {
wrapper: withNuqsTestingAdapter({
searchParams: { count: "42" },
}),
});
このテスト用アダプターを使えばよいかと思いきや、なぜかテストが失敗します。
テスト対象は下記画面のコンポーネントで、赤枠のタブは <a> タグのリンクになっており、タブごとにクエリパラメータだけ異なる URL に遷移する構成になっています。

テストでは Next Router のモックのために next-router-mock を使用しており、タブをクリックすると router の query は正しく更新されます。
ですがテスト用のアダプターでは router の値を参照していないため、 nuqs で管理している useQueryState などの値は変化せず、期待通りにタブが切り替わりません。
そのため通常の NuqsAdapter を使いたいところですが、そうすると今度は下記のようなエラーが発生します。
[nuqs] URL update rate-limited by the browser. Consider increasing `throttleMs` for key(s) `page`. TypeError: Cannot read properties of undefined (reading 'pathname')
下記の NuqsAdapter の実装を見てみると、内部で window オブジェクトから Next Router へアクセスしており、テスト環境ではこれが存在しないためにエラーが発生していました。
next-router-mock を使ってもそこまではモックしてくれないため、自前で設定することでこの問題を解決しています。
import mockRouter from 'next-router-mock';
// `window.next.router` に `next-router-mock` のモックルーターを設定
vi.stubGlobal('next', {
router: mockRouter,
});
終わりに
今回の記事で紹介した nuqs の機能は基礎的な部分で、throttle や debounce など特定のケースで便利な機能も用意されているので、是非調べてみてください。
Discussion