Next.jsでのURL状態管理を楽にするnuqsの使い方
Next.jsでフロント開発をしていると、「状態をURLに載せたい」という場面がよくあります。
- 検索条件を共有したい
- 戻る/進むで状態を復元したい
- ページをリロードしても結果を維持したい
これを素直に実装すると useSearchParams
や router.replace
を手書きすることになり、煩雑さや型崩れに悩まされます。
そこで便利なのが nuqs。
useState
ライクなAPIでURLと状態を同期し、型安全かつ履歴制御も柔軟に扱えます。
この記事では、検索フォームやフィルタ、タブ切り替え、モーダルなどの実際によく出てくる場面を例にnuqsの使い方を見ていきます。
補足:TanStack Router を使っている場合
TanStack Router を採用している場合、検索パラメータは標準で JSON パース / 型安全な検証 / スキーマ連携 が揃っており、URL状態管理だけが目的なら nuqs を追加しなくても十分です。
一方で、既に他プロジェクトで nuqs の API(useQueryState
/useQueryStates
)に慣れている、あるいは Next.js/Remix/React Router と書き味を統一したい場合には、nuqs v2 の TanStack Router アダプターを使うのも手です。
導入方法
nuqs をインストールした後、例えば、Next.js (App Router) では、下記のようにレイアウトにアダプターを設定する必要があります。
// app/layout.tsx
import { NuqsAdapter } from "nuqs/adapters/next/app";
export default function RootLayout({ children }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}
使い方サンプル
"use client";
import { useQueryState } from "nuqs";
export default function Counter() {
const [count, setCount] = useQueryState("count", { defaultValue: 0 });
return (
<div>
<button onClick={() => setCount(c => c - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
- URLに
?count=1
が同期されます -
null
を渡すとキーごと削除されます -
defaultValue
はURLに表示されず、省略可能
ケース①:検索フォーム × サーバ再描画
検索フォームの入力内容をURLに載せたいケースです。
shallow:false
にすると サーバー再描画(RSC) が走り、検索結果が最新化されます。
const [q, setQ] = useQueryState("q", {
defaultValue: "",
shallow: false,
history: "replace",
clearOnDefault: true,
});
- 入力中に無駄にリクエストを飛ばさないように
debounce
やthrottle
オプションを使うと良い - クリア時は
null
をセットしてクエリを消すとURLがすっきり
ケース②:複数条件フィルタ
カテゴリ、並び順などをまとめて管理したいときは useQueryStates
が便利です。
const [filters, setFilters] = useQueryStates(
{
category: parseAsString.withDefault("all"),
sort: parseAsString.withDefault("desc"),
},
{ urlKeys: { category: "c", sort: "s" } }
);
- URL上は
?c=books&s=asc
のように短縮 - コード上は
filters.category
/filters.sort
と意味ある名前で扱える -
history:"push"
にすると、戻るボタンでフィルタ操作を1ステップずつ追える
ケース③:タブ切り替え
タブ状態をURLに同期させると、リンク共有やリロード時の復元が自然にできます。
"use client";
import { useQueryState } from "nuqs";
export default function TabExample() {
const [tab, setTab] = useQueryState<"details" | "reviews">("tab", {
defaultValue: "details",
});
return (
<div>
<button onClick={() => setTab("details")}>詳細</button>
<button onClick={() => setTab("reviews")}>レビュー</button>
{tab === "details" ? <Details /> : <Reviews />}
</div>
);
}
- URLに
?tab=reviews
が出るので リンク共有可能 - 戻る/進むでも状態が復元される
ケース④:モーダル
モーダルもURLと同期させることで、直接リンクから開いたり、戻るボタンで閉じたりできます。
"use client";
import { useQueryState } from "nuqs";
export default function ModalExample() {
const [modal, setModal] = useQueryState("modal", {
defaultValue: "",
history: "push", // 履歴を残す
clearOnDefault: true,
});
return (
<div>
<button onClick={() => setModal("123")}>商品詳細を開く</button>
{modal && (
<div className="modal">
<h2>商品 {modal} の詳細</h2>
<button onClick={() => setModal(null)}>閉じる</button>
</div>
)}
</div>
);
}
-
?modal=123
で直接開ける(ディープリンク対応) - 「閉じる」を押すと
setModal(null)
→ クエリ削除 -
history:"push"
を使うと「閉じる」で戻るボタンが効く
パーサと型安全
nuqsはパーサを使って型安全に扱えます。
-
parseAsInteger
→ 数値 -
parseAsBoolean
→ 真偽値 -
parseAsArray
→ 配列 -
parseAsJson
→ JSON
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
withDefault
を付けることで 型と既定値を一緒に管理できます。
tRPC
やフォームバリデーションと組み合わせると堅牢になります。
サーバ側での取り回し
createLoader
で検索パラメータの定義を1か所にまとめ、サーバーコンポーネントやAPIから同じ定義を呼び出せます。Strict モードはローダー呼び出し時に指定します。
// search-params.ts
import { createLoader, parseAsInteger, parseAsString } from "nuqs/server";
export const loadSearchParams = createLoader({
q: parseAsString.withDefault(""),
page: parseAsInteger.withDefault(1),
});
// app/page.tsx(App Router の例)
import { loadSearchParams } from "./search-params";
export default async function Page({ searchParams }: { searchParams: URLSearchParams | Promise<URLSearchParams> }) {
const { q, page } = await loadSearchParams(searchParams, { strict: true });
// ...
}
- 不正な値の排除は
loadSearchParams(…, { strict: true })
の呼び出し時に有効化 - 深いRSCで共有したい場合は
createSearchParamsCache(loadSearchParams)
を使い、cache.parse(searchParams, { strict: true })
のように同様のオプションを渡せます
SEOと正規化
検索エンジンは URL のクエリパラメータの順序が違うだけでも別ページと認識することがあります。
例:
/search?q=book&page=2
/search?page=2&q=book
人間にとっては同じ検索結果ページですが、検索エンジン的には重複ページ扱いになり、SEO 的に不利になる可能性があります。
そこで nuqs の processUrlSearchParams
を使うと、クエリキーの順序を自動でソートし、常に一定のURL形式に正規化できます。
import { processUrlSearchParams } from "nuqs/server";
const url = processUrlSearchParams("/search?page=2&q=book");
// => "/search?q=book&page=2"
このようにして クエリ順序を統一しておくと、検索エンジンにもユーザーにも扱いやすいURLを維持できます。
テスト
NuqsTestingAdapter
を使えば、テスト環境でURLを偽装できます。
import { NuqsTestingAdapter } from "nuqs/adapters/testing";
- 初期クエリを与えて状態を確認
- イベント後にURLが正しく更新されるかをアサート可能
まとめ
nuqsを使うことで「URL=状態」という設計をシンプルに実現でき、検索フォームや複数条件フィルタ、モーダルやタブといった実務で頻出するパターンに対応しやすくなります。
導入の際には、その状態が本当にURLに載せるべきものかどうかを見極めることが重要です。適切に扱えば、URLを介した共有や再現性、UXの向上に大きく貢献してくれるはずです。
Discussion