Disclaimer: This article was written by AI and may contain inaccuracies or errors.
Table of contents
はじめに
React アプリケーションで状態管理を行う際、通常は useState や useReducer を使いますが、この状態を URL に反映させたいケースがあります。例えば、検索フォームの入力値やフィルタリングの条件などを URL に含めることで、ページをリロードしても状態が保持されたり、URL を共有することで他の人と同じ状態を再現できたりします。
しかし、URL のクエリパラメータと React の state を手動で同期させるのは意外と面倒です。useSearchParams や useRouter を使って自分で実装することもできますが、型安全性や履歴管理など、考慮すべき点が多くあります。
そんな課題を解決してくれそうなのが nuqs です。このライブラリは、URL のクエリパラメータを useState のような感覚で扱えるようにしてくれます。
今回、このライブラリを実際に試してみたので、その体験をレポートします。
nuqs とは
nuqs は、URL のクエリパラメータを React の state として扱えるようにするライブラリです。
主な特徴
- useState ライクな API -
useQueryStateフックで、useStateと同じような感覚で使える - 型安全性 - 組み込みのパーサーで、文字列・数値・真偽値・配列などを型安全に扱える
- フレームワーク対応 - Next.js(App Router / Pages Router)、React Router、Remix、TanStack Router など幅広く対応
- 履歴管理 - URL の変更を履歴に追加するか置き換えるかを選べる
- サーバーコンポーネント対応 - Next.js の Server Component でも型安全に使える
- React Transition 対応 -
useTransitionと組み合わせて、ローディング状態を管理できる
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 は、シンプルで使いやすく、よく考えられたライブラリだと感じました。特に以下の点が印象的でした:
- useState との親和性 - React の基本的な知識がそのまま活かせる
- 型安全性 - URL のクエリパラメータを型安全に扱える
- フレームワーク対応 - Next.js だけでなく、様々なフレームワークで使える
URL を state の source of truth として扱うという思想が一貫していて、理解しやすいライブラリだと思います。
どんな場合に使えそうか
私の経験から、以下のような場合に特に向いていると思います:
- 検索フォームやフィルタリング - 検索条件を URL で共有したい場合
- ページネーション - 現在のページ番号を URL に含めたい場合
- タブの切り替え - アクティブなタブを URL で管理したい場合
- ダッシュボードの設定 - 表示設定やフィルタを URL に保存したい場合
- ウィザード形式のフォーム - ステップごとに URL を変えて、戻るボタンで前のステップに戻れるようにしたい場合
逆に、以下のような場合は別の方法が適している可能性があります:
- 機密情報を含む状態 - URL は履歴に残るため、機密情報は含めるべきではない
- 非常に大きなデータ - URL の長さには限界があるため、大きなデータには向いていない
- 一時的な UI 状態 - モーダルの開閉など、URL に反映する必要がない状態
個人的な感想
実際に動かして試してみると、ドキュメントを読むだけでは分からなかった細かい挙動(例: 履歴管理の違いや、配列のパース方法)が体感できて、理解が深まりました。
特に、サーバーコンポーネントでも使えるという点は、Next.js の App Router を使う上で大きな価値があると思います。
URL の状態管理に課題を感じている方や、検索フォームやフィルタリング機能を実装する際に、試してみる価値があると思います。
参考リンク
nuqs を実際に試してみて、URL のクエリパラメータを React の state として扱う体験が、想像以上にスムーズであることが分かりました。
この記事が、同じようにこのライブラリに興味を持った方の参考になれば幸いです。