0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

はじめに

よくユーザーから受け付けた値を同一ページのクエリパラメータに設定したいことがあります。例えば、検索ページなどです。
検索ページには、ユーザーが入力するクエリのフォームがあり、そこに入力した値を元に検索することあります。その時、ユーザーからのクエリを同一ページのクエリパラメータとして保持しておけば、検索結果を共有したいときはその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と同様の方法でクエリパラメータを管理できます。

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

nativeとnuqsでのコード比較

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

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

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

アプリの概要

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

image.png

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

上記画面は、赤枠で囲った以下の3つのコンポーネントで実現されています。

  • Search Component: 検索フォームの入力を取得してクエリパラメータに設定する
  • Tags Component: チェックがはいったタグのラベル名をクエリパラメータに設定する
  • Board Component: クエリパラメータからquerytags2つのクエリパラメータを取得して、その内容を表示する。

Screenshot 2024-02-25 at 18.36.56.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で移動する処理を明示的に書く必要があります。

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) を実行するだけで、現在のページのクエリパラメータが変化します。遷移を意識せずに実装でき、非常に簡潔ですね。

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の場合

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= のように)。

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 から現在のクエリパラメータを取得します。

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しています。

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でも使える便利な関数が用意されています。興味のある方はぜひ確認してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?