これは 農工大アドベントカレンダー第2会場 の記事です。

はじめに

自作の楽譜管理アプリの検索Queryパラメータの管理に nuqs を導入した事例を紹介します。

nuqs とは

React の型安全な検索パラメータ状態マネージャー です。

きっかけ

こちらの動画で非常にわかりやすく解説されており、魅力を感じました。 https://www.youtube.com/watch?v=NYJgymLON2Q

これを見て、現在開発中の自作楽譜管理アプリへの導入を決めました。

背景:楽譜管理アプリのモダン化

元々 Flask で作成していた楽譜管理アプリ(参考記事)を、React 等のモダンな Web 技術で作り直しています。 Web アプリケーションらしく、URL のクエリパラメータと UI の状態を同期させるために nuqs を採用しました。

nuqs の導入

nuqs を実際に使ってみて良かった点、主な機能について解説します。

1. 型安全 (Type Safety) と バリデーション (Validation)

URL パラメータを扱う際、通常は文字列として取得して変換・検証が必要ですが、nuqs はその手間を大幅に削減してくれます。

例えば、ページネーションのための page パラメータ(数値)を扱う場合、通常は以下のような実装が必要です。

// nuqs を使わない場合(従来の手法)
const searchParams = useSearchParams()
const pageParam = searchParams.get('page')

// 文字列から数値への変換、nullチェック、NaNチェック、デフォルト値の設定...
const page = pageParam ? parseInt(pageParam, 10) : 1
const safePage = isNaN(page) ? 1 : page

これが nuqs を使うと、定義ファイルで宣言するだけで済みます。 パラメータの型定義を一元管理できます。

import { 
  createSearchParamsCache,
  parseAsString,
  parseAsInteger
} from 'nuqs/server'

export const searchParamsParsers = {
  // ... 他の定義

  // Pagination
  // 自動的に整数にパースされ、値がない場合は 1 になる
  page: parseAsInteger.withDefault(1),
}

パラメータの数が増えても、それぞれの型とデフォルト値を一度定義するだけでよく、非常に簡単です。

2. 即時反映 (Shallow Routing) と デバウンス (Debounce)

リアルタイム検索の理想と現実

ユーザーが検索フォームに文字を入力するたびに、結果を「即座に」反映させたい(リアルタイム検索)という要求はよくあります。しかし、これを愚直に実装すると、次のような問題が発生しがちです。

  1. ユーザー体験の低下 (UX):

    • URLの更新によるページ全体のリロード: クエリパラメータを変更するたびにページ全体が再読み込みされると、入力中のフォーカスが外れたり、画面がちらついたりして、ユーザーは大きなストレスを感じます。
    • 過度なURL更新: 入力の一文字ごとにURLが更新されると、ブラウザの履歴が汚れたり、リンク共有時に意図しない中間状態が共有されたりする可能性があります。
  2. パフォーマンスの悪化 (Performance):

    • サーバーへの無駄なリクエスト: 「あ」→「あい」→「あいす」と入力するたびにAPIリクエストがサーバーに送信されると、サーバーへの負荷が跳ね上がり、API制限に抵触するリスクも高まります。これはリソースの無駄遣いであり、結果的にアプリの応答速度も低下させます。

これらの問題に対処するためには、本来「Shallow Routing(URLは更新するがページは再読み込みしない)」と「Debounce(入力が一定時間止まるまで処理を遅延させる)」といったロジックを自前で実装する必要があります。Reactでこれらを組み合わせようとすると、useEffectsetTimeout を用いた複雑な状態管理やクリーンアップ処理が必要となり、コードが肥大化し保守性が低下する傾向にあります。

nuqs ならシンプルに解決

nuqs は、これらの複雑な課題を useQueryState のオプションを通じて非常にシンプルに解決します。たった数行の設定で、理想的なリアルタイム検索体験を実現できます。

// components/SearchBar.tsx

  const [urlQuery, setUrlQuery] = useQueryState('query', searchParamsParsers.query.withOptions({
    limitUrlUpdates: debounce(230), // 入力が止まってから230ms後にのみURLを更新
    shallow: !autoSync // URLは更新するが、Next.jsのクライアントサイド遷移(ページリロードなし)を実行
  }))

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // UIの状態を更新するだけでOK。URLへの反映やデバウンスは nuqs が自動で処理。
    const val = e.target.value
    setUrlQuery(val || null)
  }

このように、「入力は即座に反映したいが、無駄な処理やURL更新は避けたい」というリアルタイム検索のジレンマを、nuqs は強力かつ簡潔なAPIで解決してくれます。これにより開発者は、複雑な低レベルの実装に頭を悩ませることなく、ユーザー体験の向上に集中できます。

クエリのシリアライズ

createSerializer を使うことで、定義したパーサーに基づいたクエリ文字列を安全に生成して遷移させることができます。

// components/SearchBar.tsx

import { createSerializer } from 'nuqs'
import { searchParamsParsers } from '@/lib/searchParams'

// ...

  const serialize = createSerializer(searchParamsParsers)

  const handleSearch = () => {
    // 定義に基づいてクエリパラメータをシリアライズして遷移
    router.push('/search' + serialize({ query: urlQuery }));
  }

まとめ

nuqs は検索機能を実装する上で非常に強力なツールです。型安全、バリデーション、そしてUXを向上させる機能が手軽に実装できます。