Skip to content
Go back

nuqs を試してみた - URL のクエリパラメータを React の state のように扱えるライブラリ

はと🐤テック

Disclaimer: This article was written by AI and may contain inaccuracies or errors.

Table of contents

Open Table of contents

はじめに

React アプリケーションで状態管理を行う際、通常は useState や useReducer を使いますが、この状態を URL に反映させたいケースがあります。例えば、検索フォームの入力値やフィルタリングの条件などを URL に含めることで、ページをリロードしても状態が保持されたり、URL を共有することで他の人と同じ状態を再現できたりします。

しかし、URL のクエリパラメータと React の state を手動で同期させるのは意外と面倒です。useSearchParams や useRouter を使って自分で実装することもできますが、型安全性や履歴管理など、考慮すべき点が多くあります。

そんな課題を解決してくれそうなのが nuqs です。このライブラリは、URL のクエリパラメータを useState のような感覚で扱えるようにしてくれます。

今回、このライブラリを実際に試してみたので、その体験をレポートします。

nuqs とは

nuqs は、URL のクエリパラメータを React の state として扱えるようにするライブラリです。

主な特徴

URL を状態の source of truth(信頼できる唯一の情報源)として扱うという、シンプルで分かりやすい思想のライブラリだと思います。

セットアップ

まずは npm でインストールします。

npm install nuqs

または

pnpm install nuqs

Next.js の App Router を使う場合は、ルートレイアウトに NuqsAdapter を追加します。

// app/layout.tsx
import { NuqsAdapter } from "nuqs/adapters/next/app";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  );
}

これで準備完了です。

実際に試してみる

実際にどのように動作するか確かめるため、Next.js でデモアプリケーションを作成して試してみました。

1. 基本的な使い方

まずは、文字列、整数、真偽値を扱う基本的な例を試してみました。

"use client";

import { useQueryState, parseAsInteger, parseAsBoolean } from "nuqs";

export default function Home() {
  // 文字列の state
  const [name, setName] = useQueryState("name");

  // 整数の state(デフォルト値: 0)
  const [age, setAge] = useQueryState("age", parseAsInteger.withDefault(0));

  // 真偽値の state
  const [subscribe, setSubscribe] = useQueryState("subscribe", parseAsBoolean);

  return (
    <div>
      <h1>nuqs Demo</h1>

      {/* Name input */}
      <input
        value={name || ""}
        onChange={e => setName(e.target.value || null)}
        placeholder="Enter your name"
      />

      {/* Age input */}
      <input
        type="number"
        value={age}
        onChange={e => setAge(parseInt(e.target.value) || 0)}
      />

      {/* Subscribe checkbox */}
      <input
        type="checkbox"
        checked={subscribe || false}
        onChange={e => setSubscribe(e.target.checked || null)}
      />
    </div>
  );
}

このコードを実行すると、input に入力した値が自動的に URL に反映されます。

例えば、name に「Alice」、age に「30」、subscribe にチェックを入れると、URL は次のようになります:

http://localhost:3000/?name=Alice&age=30&subscribe=true

useState とほぼ同じ感覚で使えるのが印象的でした。値を null にすることで、URL からそのパラメータを削除できる点も便利だと思います。

2. 配列のパース

次に、配列を扱う例を試してみました。タグのリストを URL で管理するケースを想定しています。

"use client";

import { useQueryState, parseAsArrayOf, parseAsString } from "nuqs";
import { useState } from "react";

export default function AdvancedPage() {
  // 配列の state
  const [tags, setTags] = useQueryState(
    "tags",
    parseAsArrayOf(parseAsString, ",")
  );

  const [newTag, setNewTag] = useState("");

  const addTag = () => {
    if (newTag && !tags?.includes(newTag)) {
      setTags([...(tags || []), newTag]);
      setNewTag("");
    }
  };

  const removeTag = (tagToRemove: string) => {
    setTags(tags?.filter(t => t !== tagToRemove) || null);
  };

  return (
    <div>
      <h2>Tags</h2>
      <input
        value={newTag}
        onChange={e => setNewTag(e.target.value)}
        onKeyPress={e => e.key === "Enter" && addTag()}
      />
      <button onClick={addTag}>Add Tag</button>

      {tags?.map(tag => (
        <span key={tag}>
          {tag}
          <button onClick={() => removeTag(tag)}>×</button>
        </span>
      ))}
    </div>
  );
}

例えば、「react」「typescript」「nextjs」というタグを追加すると、URL は次のようになります:

http://localhost:3000/advanced?tags=react,typescript,nextjs

配列がカンマ区切りで URL に格納され、削除や追加も自動的に反映されました。配列の操作も state と同じ感覚でできるのが便利だと感じました。

3. 複数の state を同時に更新

useQueryStates(複数形)を使うと、複数の state を一度に更新できます。

"use client";

import { useQueryStates, parseAsString, parseAsInteger } from "nuqs";
import { useTransition } from "react";

export default function AdvancedPage() {
  const [isPending, startTransition] = useTransition();

  const [filters, setFilters] = useQueryStates({
    category: parseAsString,
    minPrice: parseAsInteger,
    maxPrice: parseAsInteger,
  });

  const applyFilters = () => {
    startTransition(() => {
      setFilters({
        category: "electronics",
        minPrice: 100,
        maxPrice: 500,
      });
    });
  };

  return (
    <div>
      <button onClick={applyFilters} disabled={isPending}>
        Apply Filters
      </button>
      {isPending && <p>Updating URL...</p>}
    </div>
  );
}

ボタンをクリックすると、3つのパラメータが同時に URL に反映されます:

http://localhost:3000/advanced?category=electronics&minPrice=100&maxPrice=500

useTransition と組み合わせることで、URL の更新中にローディング表示を出すこともできました。React 18 の新機能との統合が考えられている点が良いと思います。

4. 履歴管理

URL の変更方法には、デフォルトの「replace」(履歴を置き換える)と「push」(履歴に追加する)の2つがあります。

"use client";

import { useQueryState } from "nuqs";

export default function HistoryPage() {
  // デフォルト: replace(履歴を置き換える)
  const [replaceValue, setReplaceValue] = useQueryState("replace");

  // push(履歴に追加する)
  const [historyValue, setHistoryValue] = useQueryState("history", {
    history: "push",
  });

  return (
    <div>
      <h2>Replace History (Default)</h2>
      <input
        value={replaceValue || ""}
        onChange={e => setReplaceValue(e.target.value || null)}
      />

      <h2>Push to History</h2>
      {["apple", "banana", "cherry"].map(fruit => (
        <button key={fruit} onClick={() => setHistoryValue(fruit)}>
          {fruit}
        </button>
      ))}
    </div>
  );
}

history: 'push' を指定すると、ブラウザの戻るボタンで以前の状態に戻れるようになります。実際に試してみると、フルーツのボタンを「apple → banana → cherry」の順にクリックした後、戻るボタンを押すと「cherry → banana → apple」と戻っていくことが確認できました。

これは、ウィザード形式のフォームやステップバイステップのUIで便利そうだと思いました。

5. サーバーコンポーネントでの使用

Next.js の Server Component では、nuqs/server を使って型安全に searchParams を扱えます。

// app/server/page.tsx
import {
  createSearchParamsCache,
  parseAsString,
  parseAsInteger,
} from "nuqs/server";

// searchParams のスキーマを定義
const searchParamsCache = createSearchParamsCache({
  query: parseAsString.withDefault(""),
  page: parseAsInteger.withDefault(1),
  category: parseAsString,
});

export default async function ServerPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  // サーバー側でパースして型安全に扱う
  const params = await searchParams;
  const { query, page, category } = searchParamsCache.parse(params);

  // パースされた値を使ってデータ取得など
  const results = await fetchData(query, page, category);

  return (
    <div>
      <h1>Search Results</h1>
      <p>Query: {query}</p>
      <p>Page: {page}</p>
      <p>Category: {category || "All"}</p>
      {/* ... */}
    </div>
  );
}

サーバーコンポーネントでも型安全に扱えるのは、Next.js を使う上で大きなメリットだと感じました。

実際に http://localhost:3000/server?query=react&category=books&page=2 にアクセスすると、サーバー側でパースされた値が表示されることを確認できました:

{
  "query": "react",
  "page": 2,
  "category": "books"
}

良かった点

1. 直感的な API

useState と同じ感覚で使えるため、学習コストがほとんどありませんでした。既存の React の知識がそのまま活かせる点が良いと思います。

2. 型安全性

parseAsInteger、parseAsBoolean、parseAsArrayOf などの組み込みパーサーにより、URL のクエリパラメータを型安全に扱えます。文字列として扱うのではなく、適切な型に変換してくれるのは安心感があります。

3. URL を共有できる

状態が URL に含まれるため、URL をコピー&ペーストするだけで、他の人と同じ状態を共有できます。検索結果やフィルタリング条件など、共有したいケースは多いと思います。

4. リロードしても状態が保持される

ページをリロードしても、URL からパラメータを読み取って状態が復元されます。フォームの途中でページを離れても、戻ってきたときに続きから始められるのは便利だと感じました。

5. サーバーコンポーネント対応

Next.js の Server Component でも使えるのは、現代的なアプリケーション開発において重要だと思います。クライアント側とサーバー側で同じライブラリを使えるのは一貫性があって良いです。

6. 履歴管理の柔軟性

history: 'replace' と history: 'push' を選べるため、用途に応じて適切な挙動を選択できます。デフォルトが replace なのも、多くのケースで妥当な選択だと思います。

7. React Transition との統合

useTransition と組み合わせることで、URL 更新中のローディング状態を管理できます。React 18 の新機能との統合が考えられている点が印象的でした。

8. 充実したドキュメント

GitHub の README が非常に詳しく、使い方だけでなく設計思想や背景まで説明されています。初めて使う際も、安心して試すことができました。

気づいた点

1. URL が長くなる可能性

状態を全て URL に含めるため、フィルタリング条件が多い場合などは URL が長くなることがあります。これ自体は設計上の自然な結果ですが、あまりに多くの状態を URL に含めるのは適切ではないケースもあると思います。

2. Client Component が必要

useQueryState を使うためには Client Component('use client' ディレクティブ)が必要です。Next.js の App Router では Server Component がデフォルトなので、この点は注意が必要だと思いました。

ただし、Server Component では nuqs/server を使えるため、使い分けができるようになっています。

3. デフォルト値の扱い

パーサーに .withDefault() を指定しないと、値がない場合は null になります。これは予想通りの挙動ですが、最初は少し戸惑うかもしれません。

ただ、エラーメッセージやドキュメントが親切なので、すぐに理解できました。

4. アダプターのセットアップ

Next.js の場合、ルートレイアウトに NuqsAdapter を追加する必要があります。これは一度だけの作業ですが、忘れるとエラーになるので、セットアップ時に注意が必要だと思います。

まとめ

私が感じたこと

nuqs は、シンプルで使いやすく、よく考えられたライブラリだと感じました。特に以下の点が印象的でした:

URL を state の source of truth として扱うという思想が一貫していて、理解しやすいライブラリだと思います。

どんな場合に使えそうか

私の経験から、以下のような場合に特に向いていると思います:

逆に、以下のような場合は別の方法が適している可能性があります:

個人的な感想

実際に動かして試してみると、ドキュメントを読むだけでは分からなかった細かい挙動(例: 履歴管理の違いや、配列のパース方法)が体感できて、理解が深まりました。

特に、サーバーコンポーネントでも使えるという点は、Next.js の App Router を使う上で大きな価値があると思います。

URL の状態管理に課題を感じている方や、検索フォームやフィルタリング機能を実装する際に、試してみる価値があると思います。

参考リンク


nuqs を実際に試してみて、URL のクエリパラメータを React の state として扱う体験が、想像以上にスムーズであることが分かりました。

この記事が、同じようにこのライブラリに興味を持った方の参考になれば幸いです。


Share this post on:

Previous Post
pino-pretty を試してみた - 開発時に読みやすいログを出力する
Next Post
dotenv-safe を試してみた - .env.example で環境変数の設定漏れを防ぐ