nuqsで楽々クエリパラメータ管理

本記事のコードは以下から参照できます。

https://github.com/haru-256/nuqs-sandbox

# はじめに

よくユーザーから受け付けた値を同一ページのクエリパラメータに設定したいことがあります。例えば、検索ページなどです。
検索ページには、ユーザーが入力するクエリのフォームがあり、そこに入力した値を元に検索することあります。その時、ユーザーからのクエリを同一ページのクエリパラメータとして保持しておけば、検索結果を共有したいときはそのURLを共有すれば良く、非常に便利です。例えば、Googleでは https://www.google.com/search?q=hoge のように、`q`パラメータに入力値を保持しています。

具体的には以下のような動きになります。
- ユーザーが検索フォームに値を入力
- 入力された値を同一ページのクエリパラメータに保存
- クエリパラメータの値（=入力された値）を元に、検索

上記の動きは、Next.jsでは `useRouter` を用いて、入力された値をクエリパラメータに含んだURLへ移動することで実現できます。
この記事ではクエリパラメータとの同期を、上記の方法（以下ではNativeと呼びます）の代わりに、より簡単・直感的な方法として、nuqsというライブラリを紹介します。

# nuqsとは
nuqsは2020年ごろにリリースされたライブラリで、`useState`と同様の方法でクエリパラメータの更新、値取得を行うフックを提供しています。また、クエリパラメータを取得すると、値は通常stringと解釈されるのですが、intやArrayなど任意の型へのキャストができます。
nuqsでも裏側では`useRouter`が使用されていますが、その存在をうまく隠蔽されており特に気にせず`useState`と同様の方法でクエリパラメータを管理できます。

https://github.com/47ng/nuqs

実際にnuqsの使用方法を、nativeの方法（`useRouter`を自分で触る方法）とnuqsを使う方法の２つを比較しながら解説していきます。


# nativeとnuqsでのコード比較

nativeとnuqsそれぞれの実装を比較するために、簡単なWebアプリを作成しました。コードは以下の通りです。

https://github.com/haru-256/nuqs-sandbox

このWebアプリでは以下2つのページが実装されており、それぞれの動き自体は全く同じで、内部実装がnativeかnuqsかで違っています。

- /native : native実装
- /nuqs : nuqsを使用している

## アプリの概要

/native と /nuqs ともにですが、以下のような画面を提供しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200359/6458f682-c028-16ff-b24a-027bb323c932.png)

上部の検索フォーム文字を入力すると、入力されたクエリがクエリパラメータに追加され、また下部の「query := 」にそのクエリパラメータが表示されます。また、中部のタグをクリックすると、該当tagのラベル名がクエリパラメータに追加されます。tagは複数選択可能で、2個以上選択されている場合はラベル名をカンマ区切りで連結し、その値がクエリパラメータに設定されます。

上記画面は、赤枠で囲った以下の3つのコンポーネントで実現されています。
- `Search Component`: 検索フォームの入力を取得してクエリパラメータに設定する
- `Tags Component`: チェックがはいったタグのラベル名をクエリパラメータに設定する
- `Board Component`: クエリパラメータから`query`と`tags`２つのクエリパラメータを取得して、その内容を表示する。

![Screenshot 2024-02-25 at 18.36.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200359/7027e1be-48ee-912c-2ea5-0ee8e267fa18.png)

上記画面のように、検索クエリ="hotate", 検索タグ=Ract・Next.js が選ばれている場合、URLは以下のようになります。

- nativeの場合: /native?query=hotate&tags=React%2CNext.js
- nuqsを使用する場合: /nuqs?tags=React,Next.js&query=hotate

ユーザーが入力する検索クエリや検索タグが変化することで、動的にクエリパラメータも変化します。

各コンポーネントごとにnativeとnuqsそれぞれの実装を見ていきます。

## Search Component

まずはSearch Componentです。こちらはユーザーが入力した情報をクエリパラメータに追加するコンポーネントです。

nativeの場合は以下のように、入力情報を元に `URLSearchParams`を使用してクエリパラメータを追加し、そのクエリパラメータへ`replace`で移動する処理を明示的に書く必要があります。

```tsx:src/features/Native/Search/index.tsx
"use client";

import Search from "@/components/Search";
import { useRouter } from "next/navigation";
import { useSearchParams, usePathname } from "next/navigation";
import React from "react";

export default function NativeSearch() {
  const { replace } = useRouter();
  const pathName = usePathname();
  const searchParams = useSearchParams();
  const query = searchParams.get("query");
  // 検索ボックスの値が変更されたときに呼ばれる関数
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    console.log(value);
    const params = new URLSearchParams(searchParams);
    if (value) {
      params.set("query", value);
    } else {
      params.delete("query");
    }
    const paramsString = params.toString();
    replace(paramsString ? `${pathName}?${params.toString()}` : pathName);
  };

  return <Search value={query} onChange={onChange} />;
}
```

nuqsを使用する場合では、`replace`による遷移を意識する必要はありません。nuqsではクエリパラメータの状態を`useQueryState`と呼ばれるフックで管理しています。この返り値は `useState` と同様です。1つ目は現在のクエリパラメータの値であり、2つ目はクエリパラメータを設定するdispatchとなります。
使い方は`useState`と似ており、以下のように `setQuery(value)` を実行するだけで、現在のページのクエリパラメータが変化します。遷移を意識せずに実装でき、非常に簡潔ですね。

```tsx:src/features/Nuqs/Search/index.tsx
"use client";

import Search from "@/components/Search";
import React from "react";
import { useQueryState } from "nuqs";

export default function NuqsBoard() {
  const [query, setQuery] = useQueryState("query");
  // 検索ボックスの値が変更されたときに呼ばれる関数
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    console.log(value);
    if (value) {
      setQuery(value);
    } else {
      setQuery(null);
    }
  };

  return <Search value={query} onChange={onChange} />;
}
```

## Tags Component

次はTags Componentについてです。こちらもSearch Componentと同じく、チェックがついているtagのラベル名を元にクエリパラメータを追加します。

nativeの場合
```tsx:src/features/Native/Tags/index.tsx
"use client";

import React from "react";
import Tag from "@/components/Tag";
import { useRouter } from "next/navigation";
import { useSearchParams, usePathname } from "next/navigation";

const labels = ["React", "Next.js", "nuqs", "TypeScript", "Tailwind CSS"];

export default function NativeTags() {
  const { replace } = useRouter();
  const pathName = usePathname();
  const searchParams = useSearchParams();
  const tags = searchParams.get("tags")?.split(",");

  return (
    <div className="flex flex-wrap gap-2">
      {labels.map((label) => (
        <Tag
          key={label}
          label={label}
          isChecked={tags ? tags.includes(label) : false}
          // 各タグがクリックされたときに呼ばれる関数
          onClick={() => {
            const params = new URLSearchParams(searchParams);
            if (tags) {
              // 選択されているタグをクリックしたら選択解除
              if (tags.includes(label)) {
                const filteredTags = tags.filter((tag) => tag !== label);
                if (filteredTags.length) {
                  params.set("tags", filteredTags.join(","));
                } else {
                  params.delete("tags");
                }
              } else {
                params.set("tags", [...tags, label].join(","));
              }
            } else {
              params.set("tags", label);
            }

            const paramsString = params.toString();
            replace(
              paramsString ? `${pathName}?${params.toString()}` : pathName
            );
          }}
        />
      ))}
    </div>
  );
}
```

nuqsを使用する場合は以下の通りです。nuqsの`set`系dispatchでは、`null`を与えるとそのクエリパラメータを削除してくれます。これは地味に便利です。というのものnativeでは`URLSearchParams`を使ってクエリパラメータを管理するのですが、これには`null`を設定することができません。また、代わりに空文字列`""`を設定すると、愚直に空文字列がクエリパラメータに追加されてしまいます（例えば、/native?q= のように）。

```tsx:src/features/Nuqs/Tags/index.tsx
"use client";

import React from "react";
import Tag from "@/components/Tag";
import { useQueryState, parseAsArrayOf, parseAsString } from "nuqs";

const labels = ["React", "Next.js", "nuqs", "TypeScript", "Tailwind CSS"];

export default function NuqsTags() {
  const [tags, setTags] = useQueryState("tags", parseAsArrayOf(parseAsString));

  return (
    <div className="flex flex-wrap gap-2">
      {labels.map((label) => (
        <Tag
          key={label}
          label={label}
          isChecked={tags ? tags.includes(label) : false}
          onClick={() => {
            if (tags) {
              // 選択されているタグをクリックしたら選択解除
              if (tags.includes(label)) {
                const filteredTags = tags.filter((tag) => tag !== label);
                setTags(filteredTags.length ? filteredTags : null);
              } else {
                setTags([...tags, label]);
              }
            } else {
              setTags([label]);
            }
          }}
        />
      ))}
    </div>
  );
}
```

## Board Component

次にクエリパラメータを取得するBoard Componentについてです。

nativeの場合は以下の通りです。Next.jsの `useSearchParams` から現在のクエリパラメータを取得します。

```tsx:src/features/Native/Board/index.tsx
"use client";

import { useSearchParams } from "next/navigation";
import Board from "@/components/Board";

export default function NativeBoard() {
  const searchParams = useSearchParams();

  return (
    <Board
      search={searchParams.get("query")}
      tags={searchParams.get("tags")?.split(",") || null}
    />
  );
}
```

nuqsの場合は以下の通りです。複数のクエリパラメータを取得するには、`useQueryStates`が便利です。また、tagsのように、クエリパラメータがstring以外の型にキャストしたいときはparserを設定することができます。
例えば、以下ではtagsはstring型のArrayにparseしています。

```tsx:src/features/Nuqs/Board/index.tsx
"use client";

import { useQueryStates, parseAsArrayOf, parseAsString } from "nuqs";
import Board from "@/components/Board";

export default function NuqsBoard() {
  const [query, _] = useQueryStates({
    query: parseAsString,
    tags: parseAsArrayOf(parseAsString),
  });

  return <Board query={query.query} tags={query.tags} />;
}
```

# まとめ
この記事では、クエリパラメータを簡単に取得・設定できる `nuqs`を紹介しました。`nuqs`は今回紹介したClient Componentだけでなく、Server Componentでも使える便利な関数が用意されています。興味のある方はぜひ確認してみてください。

https://github.com/47ng/nuqs