本記事のコードは以下から参照できます。
はじめに
よくユーザーから受け付けた値を同一ページのクエリパラメータに設定したいことがあります。例えば、検索ページなどです。
検索ページには、ユーザーが入力するクエリのフォームがあり、そこに入力した値を元に検索することあります。その時、ユーザーからのクエリを同一ページのクエリパラメータとして保持しておけば、検索結果を共有したいときはその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 ともにですが、以下のような画面を提供しています。
上部の検索フォーム文字を入力すると、入力されたクエリがクエリパラメータに追加され、また下部の「query := 」にそのクエリパラメータが表示されます。また、中部のタグをクリックすると、該当tagのラベル名がクエリパラメータに追加されます。tagは複数選択可能で、2個以上選択されている場合はラベル名をカンマ区切りで連結し、その値がクエリパラメータに設定されます。
上記画面は、赤枠で囲った以下の3つのコンポーネントで実現されています。
-
Search Component
: 検索フォームの入力を取得してクエリパラメータに設定する -
Tags Component
: チェックがはいったタグのラベル名をクエリパラメータに設定する -
Board Component
: クエリパラメータからquery
とtags
2つのクエリパラメータを取得して、その内容を表示する。
上記画面のように、検索クエリ="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
で移動する処理を明示的に書く必要があります。
"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)
を実行するだけで、現在のページのクエリパラメータが変化します。遷移を意識せずに実装でき、非常に簡潔ですね。
"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の場合
"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= のように)。
"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
から現在のクエリパラメータを取得します。
"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しています。
"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でも使える便利な関数が用意されています。興味のある方はぜひ確認してみてください。