0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

URL クエリだけで UI 状態を全部再現する「nuqs」実践ガイド(Next.js App Router)

0
Posted at

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 は「URL と UI の同期」という地味だけど重要な課題を
めちゃくちゃ綺麗に解決してくれるライブラリです。

  • 技術ブログ
  • 投稿検索 UI
  • SaaS の管理画面
  • フロントエンド学習用の教材

など、幅広く応用できます。

ぜひ、あなたのプロジェクトでも試してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?