nuqsでURLパラメータ管理が劇的に楽になる話

ReactでURLパラメータを手動管理する面倒から解放。型安全で共有可能な状態管理を実現するnuqsの使い方と実装例を詳しく解説します。

nuqsでURLパラメータ管理が劇的に楽になる話

はじめに: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 サイトの検索機能
  • ダッシュボードのフィルター
  • データテーブルのソート・ページング
  • 共有可能な設定画面

参考リンク

BP

BitPluse Team

Building the future of software development, one line at a time.

Keep Learning

Explore more articles and expand your knowledge

View All Articles