はじめに:URLパラメータ管理の苦悩
EC サイトの商品一覧ページを開発していると想像してください。価格帯でフィルタリングし、「人気順」でソートして、3 ページ目を表示している状態。この完璧な検索結果を同僚に共有したい、でも URL を送っても初期状態のページしか表示されない…。
こんな経験、ありませんか?
// 従来の面倒なアプローチ
const [page, setPage] = useState(1);
const [sort, setSort] = useState('popular');
const [priceRange, setPriceRange] = useState([0, 10000]);
useEffect(() => {
// URLパラメータと同期...複雑なロジック
const params = new URLSearchParams(window.location.search);
// 型変換、バリデーション、更新処理...
}, [page, sort, priceRange]);
このコードには以下のような問題があります。
- 🔴 URL と React state の二重管理
- 🔴 型安全性の欠如
- 🔴 ブラウザの戻るボタンが機能しない
- 🔴 共有・ブックマーク不可能
- 🔴 大量のボイラープレートコード
nuqsが解決する世界
nuqs(Next.js URL Query State)は、これらすべての問題を一挙に解決します。
// NuQSのエレガントなアプローチ
import { useQueryState, parseAsInteger } from 'nuqs';
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
たった 2 行。これだけで以下の機能が実現できます。
- ✅ URL に自動同期
- ✅ 型安全(TypeScript対応)
- ✅ ブラウザの戻る/進むボタン対応
- ✅ 共有・ブックマーク可能
- ✅ SSR/RSC 対応
実践例:検索フィルターの実装
1. 基本的な商品検索
'use client';
import {
useQueryState,
parseAsString,
parseAsInteger,
parseAsStringEnum
} from 'nuqs';
const sortOptions = ['popular', 'price-asc', 'price-desc'] as const;
export function ProductSearch() {
// 検索クエリ
const [query, setQuery] = useQueryState('q',
parseAsString.withDefault('')
);
// ページネーション
const [page, setPage] = useQueryState('page',
parseAsInteger.withDefault(1)
);
// ソート順
const [sort, setSort] = useQueryState('sort',
parseAsStringEnum(sortOptions).withDefault('popular')
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="商品を検索..."
/>
<select value={sort} onChange={(e) => setSort(e.target.value)}>
<option value="popular">人気順</option>
<option value="price-asc">価格:安い順</option>
<option value="price-desc">価格:高い順</option>
</select>
{/* URL: /products?q=iPhone&sort=price-asc&page=2 */}
</div>
);
}
2. 複数パラメータの一括管理
import { useQueryStates, parseAsBoolean } from 'nuqs';
export function AdvancedFilters() {
const [filters, setFilters] = useQueryStates({
category: parseAsString,
inStock: parseAsBoolean.withDefault(false),
minPrice: parseAsInteger,
maxPrice: parseAsInteger,
brands: parseAsArrayOf(parseAsString)
});
// 一括更新も簡単
const applyPreset = () => {
setFilters({
category: 'electronics',
inStock: true,
minPrice: 10000,
maxPrice: 50000,
brands: ['Apple', 'Sony']
});
};
return (
// URL: /products?category=electronics&inStock=true&minPrice=10000&maxPrice=50000&brands=Apple,Sony
<FilterUI {...filters} />
);
}
高度な機能と実装パターン
1. 履歴管理のコントロール
const [view, setView] = useQueryState('view');
// URLを置き換え(履歴に追加しない)
setView('grid', { history: 'replace' });
// 履歴に追加(デフォルト)
setView('list', { history: 'push' });
// スムーズなトランジション対応
const [isPending, startTransition] = useTransition();
const updateView = (value: string) => {
startTransition(() => {
setView(value);
});
};
2. Server Componentsでの活用
// app/products/page.tsx (Server Component)
import { parseAsInteger } from 'nuqs/server';
export default async function ProductsPage({
searchParams
}: {
searchParams: Record<string, string | string[]>
}) {
const page = parseAsInteger.parseServerSide(searchParams.page) ?? 1;
// サーバーサイドでデータ取得
const products = await fetchProducts({ page });
return <ProductList products={products} />;
}
3. カスタムパーサーの作成
import { createParser } from 'nuqs';
// 日付範囲のカスタムパーサー
const parseDateRange = createParser({
parse: (value: string) => {
const [start, end] = value.split(',');
return {
start: new Date(start),
end: new Date(end),
};
},
serialize: (value: { start: Date; end: Date }) => {
return `${value.start.toISOString()},${value.end.toISOString()}`;
},
});
// 使用例
const [dateRange, setDateRange] = useQueryState(
'dates',
parseDateRange.withDefault({
start: new Date('2025-01-01'),
end: new Date('2025-12-31'),
})
);
パフォーマンス最適化
1. デバウンス処理
import { useQueryState } from 'nuqs';
import { useDebounce } from 'use-debounce';
export function SearchInput() {
const [query, setQuery] = useQueryState('q');
const [inputValue, setInputValue] = useState(query ?? '');
const [debouncedValue] = useDebounce(inputValue, 300);
useEffect(() => {
setQuery(debouncedValue || null);
}, [debouncedValue, setQuery]);
return (
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="検索..."
/>
);
}
2. バッチ更新
// 複数の状態更新が自動的にバッチ処理される
const handleReset = () => {
// これらは1回のURL更新にまとめられる
setPage(1);
setSort('popular');
setQuery('');
setFilters(defaultFilters);
};
実装時のベストプラクティス
1. 適切な使用場面
nuqsが最適な場合
以下のような機能を実装する際に効果的です。
- 🎯 検索・フィルター機能
- 🎯 ページネーション
- 🎯 タブやビューの切り替え
- 🎯 ダッシュボードの設定
- 🎯 共有可能な状態
避けるべき場合
次のようなケースでは他の選択肢を検討してください。
- ❌ 機密情報の管理
- ❌ 大量のデータ(URL が長くなりすぎる)
- ❌ 頻繁に変更される一時的な状態
- ❌ 複雑にネストされたオブジェクト
2. SEO対策
// メタデータを動的に生成
export async function generateMetadata({ searchParams }) {
const category = parseAsString.parseServerSide(searchParams.category);
return {
title: category ? `${category}の商品一覧 | ショップ名` : '商品一覧 | ショップ名',
description: `${category || 'すべて'}のカテゴリの商品を探す`,
};
}
導入方法とセットアップ
# npm
npm install nuqs
# bun
bun add nuqs
# pnpm
pnpm add 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>
);
}
テスト戦略
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen } from '@testing-library/react';
import { ProductSearch } from './ProductSearch';
test('検索パラメータが正しく更新される', () => {
render(
<NuqsTestingAdapter searchParams={{ q: 'iPhone' }}>
<ProductSearch />
</NuqsTestingAdapter>
);
const input = screen.getByPlaceholderText('商品を検索...');
expect(input).toHaveValue('iPhone');
});
まとめ:なぜnuqsを選ぶべきか
nuqs は単なる状態管理ライブラリではありません。**「URL のための ORM」**とも呼ばれ、以下の革命的な価値を提供します:
開発者体験の向上
- 💎 型安全性 - TypeScriptとの緊密な統合
- 🚀 生産性 - ボイラープレートコードを 90%削減
- 🎨 直感的 API - useState と同じ感覚で使える
ユーザー体験の改善
- 🔗 共有可能 - URL をコピーするだけで状態を共有
- 📚 ブックマーク対応 - お気に入りの検索条件を保存
- ↩️ 履歴ナビゲーション - ブラウザの戻るボタンが期待通りに動作
パフォーマンス
- ⚡ 軽量 - わずか 4.35KB (gzip)
- 🔄 自動バッチング - 複数の更新を効率的に処理
- 🌐 SSR/RSC 対応 - Next.jsの最新機能に対応
従来の手動管理に比べて、nuqs は開発速度を向上させ、バグを減らし、より良いユーザー体験を提供します。
もしあなたが以下のような機能を開発しているなら、nuqs は必須のツールとなるでしょう。
- EC サイトの検索機能
- ダッシュボードのフィルター
- データテーブルのソート・ページング
- 共有可能な設定画面