React + Next.js を使っていると、
- 状態を URL に同期したい
- ページをリロードしてもフィルタ内容を保持したい
- URL を共有して、そのままの状態を再現したい
- デバッグしやすい UI 状態管理をしたい
こんな場面が必ず出てきます。
しかし、URL クエリと UI 状態をいい感じに同期するのは、意外と面倒です。
そこで今回紹介するのが nuqs。
Next.js App Router と相性がよく、**“URL を状態のソース・オブ・トゥルースにする”**ためのライブラリです。
この記事では、
実際に作成したデモアプリ「nuqs-explorer」のコードを使いながら、
URL × UI の完全同期を実現する方法を解説します。
🎯 このデモでできること
作成したデモページでは、次の UI 状態をすべて URL に反映しています。
◎ URL と同期する UI 状態
- 検索キーワード(q)
- カテゴリ(category)
- タグ(tags[])
- 並び替え(sort)
- お気に入りタブ(tab)
- ページ番号(page)
- 表示モード(view=grid|list)
- 詳細モーダル(itemId=xxx)
ページをリロードしても、URL を共有しても、
完全に同じ UI 状態が復元されます。
🧪 デモ・リポジトリ
🔗 GitHub
https://github.com/Kazuya-Sakashita/nuqs-explorer
🔗 デモ(Vercel)
https://nuqs-explorer.vercel.app/items
🧩 nuqs が解決する課題とは?
URL と UI の状態を同期したいとき、
純粋な Next.js のみで実現しようとすると次の課題があります。
- useSearchParams は読み取り専用
- 書き込みには router.replace() が必要
- 同期対象ごとに自前でパース&文字列化
- Boolean / Number / Array などの変換が面倒
これをほぼゼロからやるのは非効率です。
nuqs はこれを パーサーとフックで抽象化してくれます。
最小の UI 状態なら 1 行で書けます。
const [query, setQuery] = useQueryStates({
q: parseAsString.withDefault(""),
});
UI → URL、URL → UI の同期を 自動で双方向にやってくれます。
🚀 実装ポイント(サンプルコード付き)
ここでは nuqs-explorer の実装から
特に「使える!」部分だけ抜粋して紹介します。
① NuqsAdapter を layout.tsx に設定
App Router の場合は 必須。
// src/app/layout.tsx
import { NuqsAdapter } from "nuqs/adapters/next/app";
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}
② useQueryStates で URL とフォームを同期
検索 UI の状態をすべて URL 化しています。
const [query, setQuery] = useQueryStates({
q: parseAsString.withDefault(""),
category: parseAsStringEnum(["all","frontend","backend","tooling"]).withDefault("all"),
tags: parseAsArrayOf(parseAsString).withDefault([]),
sort: parseAsStringEnum(["latest","popular"]).withDefault("latest"),
tab: parseAsStringEnum(["all","favorites"]).withDefault("all"),
page: parseAsInteger.withDefault(1),
});
query.q を変えた瞬間に ?q=xxx が URL に反映され、
URL を変えると UI 側も自動で更新されます。
③ クライアント側でフィルタ・ページングを実施
App Router のページはサーバーでレンダリングされるため、
検索・絞り込みはクライアント側で行います。
const PAGE_SIZE = 5;
const pageItems = useMemo(() => {
const start = (query.page - 1) * PAGE_SIZE;
return filtered.slice(start, start + PAGE_SIZE);
}, [filtered, query.page]);
ページ番号は URL と同期しているので、
page=3 の URL をそのまま踏むと 3 ページ目が表示されます。
④ 詳細モーダルも URL ベースで制御
UI の開閉ではなく、URL の存在で決めるのがポイント。
// itemId があるならモーダルを開く
const { itemId } = searchParamsCache.parse(searchParams);
return (
<>
<ItemsList items={pageItems} />
<ItemDetailModal itemId={itemId} />
</>
);
URL がこうなると…
/items?itemId=7
アプリはページ遷移なしで モーダル表示状態で復元できます。
📦 デモのデータ構造(Mock)
mockData.ts に 15 件のアイテムを定義しており、
リアルな検索デモに使えます。
カテゴリ / タグ / お気に入り数(favorites)などが含まれます。
export type Item = {
id: string;
title: string;
description: string;
category: "frontend" | "backend" | "tooling";
tags: string[];
favorites: number;
createdAt: string;
};
実際のブログや記事の一覧にも応用できます。
📝 この記事のポイントまとめ
- nuqs を使うと URL がそのままアプリ状態になる
- ページ更新や共有でも UI を完全に復元できる
- Parsers により Boolean/Number/Array が超扱いやすい
- App Router と相性が良く、小規模〜中規模アプリで特に有効
- モーダル・検索フォーム・ページングなどで役立つ
📚 公式ドキュメント(厳選)
-
nuqs 公式
https://nuqs.dev -
Parsers
https://nuqs.dev/docs/parsers -
useQueryStates
https://nuqs.dev/docs/batching -
Next.js App Router との統合
https://nuqs.dev/docs/integration/next-app -
Next.js(App Router) 公式
https://nextjs.org/docs/app -
React 公式
https://react.dev/learn
📌 おわりに
nuqs は「URL と UI の同期」という地味だけど重要な課題を
めちゃくちゃ綺麗に解決してくれるライブラリです。
- 技術ブログ
- 投稿検索 UI
- SaaS の管理画面
- フロントエンド学習用の教材
など、幅広く応用できます。
ぜひ、あなたのプロジェクトでも試してみてください。