🪂

nuqsを使えば、useState無しでNext.jsのクエリパラメータ同期が簡潔に書ける

2024/08/20に公開

こんにちは、トラストハブのピロピロです。

突然ですが、検索フォームやフィルタなどのUIの変更に合わせて、URLのQuery paramが同期するコードをたくさん書いていてうんざりしませんか?難しいコードではないものの、状態が増えたりするたびに毎回似たようなコードが増えてきますよね。

そこで、最近弊社でリリースしたプロダクトであるトレーディングカード専門EC Cloveストア で導入したライブラリ”nuqs”が便利だったのでご紹介させていただきます。

Next.jsで検索フォームとQuery paramを同期させる時の普通のコード

検索フォームやフィルタに入力した情報に合わせてブラウザのURLを変更するという仕様は、検索結果をシェアする時にURLをシェアすることができるため便利です。

Next.jsでこの挙動を実装するには、useSearchParamsとuseStateを組み合わせてURLのquery paramを検索フォームに入力した内容に合わせて同期するコードを書くことで実現できます。以下がサンプルです

'use client'

import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'

export function Demo() {
  const router = useRouter()
  const searchParams = useSearchParams()
  const [query, setQuery] = useState(searchParams.get('q') || '')

  const updateURL = (newQuery) => {
    const newSearchParams = new URLSearchParams(searchParams)
    if (newQuery) {
      newSearchParams.set('q', newQuery)
    } else {
      newSearchParams.delete('q')
    }
    router.push(`?${newSearchParams.toString()}`)
  }

  const handleInputChange = (e) => {
    const newQuery = e.target.value
    setQuery(newQuery)
    updateURL(newQuery)
  }

  const handleClear = () => {
    setQuery('')
    updateURL('')
  }

  return (
    <>
      <input value={query} onChange={handleInputChange} />
      <button onClick={handleClear}>クリア</button>
      <p>{query}!</p>
    </>
  )
}

一方でこのままだと、フィルターオプションやページが増えると、だんだんとコンポーネントが複雑になってきてしまいます。

nuqsとは

https://nuqs.47ng.com/

nuqsは「Next.jsの型安全なsearch params stateマネージャ」と銘打たれたライブラリであり、以下のような機能を持っています

  • useSearchParamsとuseStateを組み合わせなくても、nuqsの提供するhooks経由でクエリパラメータを操作できる
  • query paramにparamに型を定義出来る
  • ネストが深いServer componentでもquery paramにアクセスしやすい
  • 4.15 kBと軽量

Client Componentでの動作だけでなく、App RouterにおけるServer Componentでの動作についてもサポートされているライブラリです。

nuqsのClient Componentでの使い方

useStateの代わりに、useQueryStateを使うことでuseStateとuseSearchParamsを組み合わせた挙動をシンプルに記述することができます。

'use client'

import { useQueryState } from 'nuqs'

export function Demo() {
  const [query, setQuery] = useQueryState('q')

  function handleInputChange(e) {
    setQuery(e.target.value)
  }

  function handleClear() {
    setQuery(null)
  }

  return (
    <>
      <input 
        value={query || ''} 
        onChange={handleInputChange} 
      />
      <button onClick={handleClear}>クリア</button>
      <p>{query}!</p>
    </>
  )
}

また、useQueryStateの第二引数にparserを指定することで、型を指定することができます。数値やDate型、あるいはリテラルを指定することができるので、フィルターUIと組み合わせて使うと便利です。

https://nuqs.47ng.com/docs/parsers

nuqsのServer Componentでの使い方

Next.jsのApp Routerでは、useSearchParamsは、Server Componentsでは利用ができません。代わりに、page.tsxではsearchParamsという変数でQuery paramsが取得できます。

https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional

一方で、Server Componentsが深くネストされていった場合は、Componentのprops経由でsearchParamsの中身を渡していく必要があります。

// app/page.tsx
export default function Page({ searchParams }: { searchParams: SearchParamsType }) {
  return (
    <div>
      <h1>メインページ</h1>
      <NestedComponent searchParams={searchParams} />
    </div>
  );
}

// app/components/NestedComponent.tsx
export default function NestedComponent({ searchParams }: { searchParams: SearchParamsType }) {
  return (
    <div>
      <h2>ネストされたコンポーネント</h2>
      <DeepNestedComponent searchParams={searchParams} />
    </div>
  );
}

// app/components/DeepNestedComponent.tsx
export default function DeepNestedComponent({ searchParams }: { searchParams: SearchParamsType }) {
  return (
    <div>
      <h3>深くネストされたコンポーネント</h3>
      <p>検索パラメータ: {JSON.stringify(searchParams)}</p>
    </div>
  );
}

nuqsではこの問題を解決するために、Server Components用の機能としてcreateSearchParamsCacheが用意されています。

https://nuqs.47ng.com/docs/server-side

createSearchParamsCacheは、ネストされたServer Components内で型安全にQuery paramにアクセスすることができる機能です。

使い方は、まずcreateSearchParamsCacheにQuery paramのスキーマを記述します。そして、page.tsx内でそのスキーマ変数の .parse にsearchParamsを入れることで、型安全にqueryにアクセスできます。

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

const searchParamsCache = createSearchParamsCache({
  q: parseAsString.withDefault(''),
  maxResults: parseAsInteger.withDefault(10)
})

export default function Page({
  searchParams
}: {
  searchParams: Record<string, string | string[] | undefined>
}) {
  const { q: query } = searchParamsCache.parse(searchParams)
  return (
    <div>
      <h1>{query} の検索結果</h1>
      <Results />
    </div>
  )
}

また、ネストされたServer Componentからも、 searchParamsCache.get でpropsを介さずにQuery paramにアクセスできます。

function Results() {
  // 子サーバーコンポーネントで型安全な検索パラメータにアクセスする:
  const maxResults = searchParamsCache.get('maxResults')
  return <span>{maxResults} 件の結果を表示</span>
}

Shallow option

デフォルトではshallow optionが有効になっており、clientで状態変更があってもサーバーはrefreshされません。このオプションをfalseにすることで、stateがsetされるたびにServer Componentで再レンダリングを行うようにできます。

これにより、状態を変更するごとに 'next/navigation' のuseRouterでrefreshやpushを明示的に呼び出す必要がなくなります。

https://nuqs.47ng.com/docs/options#shallow

また、サーバーへの短時間における連続リクエストを防ぐために、スロットルオプションも用意されています。

まとめ

弊社のプロダクトにもnuqsを導入してみて、とても便利なライブラリだと実感したので今回ご紹介させていただきました。
毎回のようにuseStateでquery paramを管理するコードを書いているのなら、nuqsを導入することでコードをすっきりとさせることを検討してみてください。

TrustHub テックブログ

Discussion