nuqsでURLSearchParamsとの分かり合えなさを解消する
こんにちは。突然ですが、URLSearchParamsを使っていて型の扱いに苦労したことはありませんか?私はあります。型の安全性が弱いんです。
最近のフロントエンド開発では、型なしでコードを書くことはほとんどありませんよね。むしろ型がないと強烈な不安に襲われることもしばしば。
const res = fetchSome() // res is any 🤮🤮🤮
だから、型がなくなることは最大限避けたいはずです。じゃあ、型がなくなってしまう要因って何でしょう?考えられる要因はたくさんありますが、基本的なWeb開発の文脈では外部から値を取得するときが多いですね。
まず思いつくのはWeb APIの型です。でも、OpenAPIをはじめとするAPIのSchema定義手法が確立され、ノウハウが溜まってきたおかげで、型の面でストレスを感じることは少なくなってきました。じゃあ次に型を失うような原因は何か。そう、URLSearchParamsです。
// /path/?count=10
const searchParams = new URLSearchParams(location.search)
const count = searchParams.get("count")
// ^ string | null 🤮🤮🤮
URLSearchParamsは型のサポートが弱い上に、ユーザーが自由に入力できる性質上、エッジケースが頻発します。その上、取り回しも難しいです。
ケース1. 全て文字列型にキャストされる
searchParams.set("count", 10) // ?count=10
count = searchParams.get("count") // => "10" 文字列になってる 🤮
ケース2. 不完全なケースが予期しづらい
// path/?name=
searchParams.get("name") // => "" と null どっちだっけ...🤔
ケース3. 消し方を間違えやすい
searchParams.set("others", null) // ?others=null 🤮
searchParams.set("others", undefined) // ?others=undefined 🤮
searchParams.set("others", "") // ?others= 🤮
searchParams.delete("others") // 正しい 🙆♂️
さらに、React Server Componentsの普及に伴って、クライアントとサーバー間の状態共有の手法として検索パラメータの使用頻度が高くなってきています。検索パラメータの型の強化が望まれる機運が高まっているんです。
前置きが長くなっちゃいましたが、これらの悩み、nuqsを使えば全部解決できます!
nuqsとは
nuqsはNext.jsの検索パラメータの状態管理ライブラリです。
一番基本的な使い方は、ローカルな状態と検索パラメータとの同期です。
構文
useQueryStateというuseStateに似た構文で状態を宣言、更新することができます。
const [name, setName] = useQueryState("name")
URLを見ると、ローカルで宣言した状態が常に検索パラメータへ反映されていることがわかります。同期の際にはuseQueryState
に渡した"name"がキーとなります。
文字列以外の型に変換
デフォルトでは文字列型として扱われますが、第二引数にnuqsが用意したparserを渡せば数値やbool値、配列など基本的な型への変換も可能です。
import { parseAsString, parseAsInteger, parseAsBoolean, parseAsArray } from 'nuqs'
// /?count=1&isPublic=true&tags=hoge,fuga
const [count, setCount] = useQueryState("count", parseAsInteger) // count is 1
const [isPublic, setIsPublic] = useQueryState("isPublic", parseAsBoolean) // isPublic is true
const [tags, setTags] = useQueryState("tags", parseAsArrayOf(parseAsString)) // tags is ["hoge", "fuga"]
初期値の指定
keyが存在しない場合やparse不可能な場合はnull
を返しますが、任意の初期値を指定することもできます。注意点として、初期値はURLには反映されません。
// /?name=
const [name, setName] = useQueryState("name", parseAsString.withDefault("")); // name is "" 。型上も string のみになる
パラメータの削除
null
をセットすることでパラメータを削除することができます。
// path is /?name=aaa
const [name, setName] = useQueryState("name", parseAsString.withDefault(""));
setName(null)
// path is /
// name is ""
これだけでも従来searchParamsをぐちゃぐちゃ書いていた箇所がスッキリする上に、自然と型安全になります。
もちろんnuqsの良さはこれだけじゃありません。
続いて私の推しポイントを紹介します!
推し1. 文字列リテラルのユニオン型を使える
フロントエンド開発では、ユニオン型はほぼ必須ですよね。例えば「性別」のように予め定めた値しか取りえないものを検索パラメータで管理したい場合、従来の方法では値の型がstring
にワイドニングされてしまい、型安全性が失われていました。
また、ユーザーがURLに任意の値を入力した場合のバリデーションも面倒でした。
nuqsを使えばこれらの問題が解決します。文字列リテラルのユニオン型を使って、型安全かつ簡単に値を管理できるんです。
男、女、その他から性別を選択するセレクトボックスの例で考えてみましょう。
const genderOptions = ["男", "女", "その他"] as const;
type Gender = (typeof genderOptions)[number];
// gender は "男" | "女" | "その他" | null しか取りえない。
const [gender, setGender] = useQueryState("gender", parseAsStringLiteral(genderOptions));
return (
<>
<p>今選択されている gender: {gender}</p>
<select
value={gender ?? ""}
onChange={(e) => setGender(e.target.value as Gender)}
>
<option value="">性別を選択</option>
{genderOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</>
);
推し2. Linkコンポーネントへのシリアライズが型安全に
Next.jsのLinkコンポーネントでhref属性に検索パラメータを指定する際、キーの名前や値の型が正しいか心配になったことはありませんか?
// keyをtypoしていないか?値は信用できるのか?
<Link href={`/path?search=${search}&cnt=${count}sortBy=${sortBy}`}>リンク</Link>
nuqsでは予め、searchParamsのschemaを定義できるので、このような心配をしなくて済みます。
import Link from "next/link";
import {
createSerializer,
parseAsInteger,
parseAsString,
parseAsStringLiteral,
} from "nuqs";
const searchParamsSchema = {
search: parseAsString,
count: parseAsInteger.withDefault(10),
sortBy: parseAsStringLiteral(["asc", "desc"] as const),
};
const serialize = createSerializer(searchParamsSchema);
// cnt は存在しないキーを指定しているのでtsエラー
// sortBy はschemaと異なる値が指定されているのでtsエラー
<Link href={serialize({ search: "foo", cnt: 20, sortBy: "time" })}>リンク</Link>
serialize({ search: "foo", count: 20, sortBy: "asc" })
// search=foo&count=20&sortBy=asc
推し3. サーバーとの同期が簡単かつ柔軟に
検索パラメータはクライアントとサーバー間で共有されることが多いですよね。従来の方法では、検索パラメータを更新した後にrouter.refresh()でサーバーの再レンダリングを促す必要がありました。
nuqsではwithOption
にshallow: false
を指定するだけで、自動的にサーバーとの同期を行えます。さらに、throttleの設定も可能なので、パフォーマンスの最適化も簡単です。
// client component
export function Client() {
const [search, setSearch] = useQueryState(
"search",
parseAsString.withDefault("").withOptions({ shallow: true }),
);
return (
<input value={search} onChange={(e) => setSearch(e.target.value)} />
);
}
export default function Page({
searchParams,
}: { searchParams: Record<string, string | string[] | undefined> }) {
return (
<div>
server: {JSON.stringify(searchParams)}
<Client />
</div>
);
}
shallow false(default) の場合
throttleはthrottleMs
で指定できます。
useQueryState(
"search",
parseAsString.withDefault("").withOptions({ shallow: true, throttleMs: 1000 }),
);
推し4. ネストしたServer Componentsで SearchParamsを取得できる
従来であれば、ネストしたServer ComponentsではSearchParamsを取得することはできず、ルートのPageからバケツリレーする必要がありました。これがかなり面倒だったのですが、nuqsではrootでSearchParamsをキャッシュすることで、末端のServer Componentsでもキャッシュを再利用する形でSearchParamsを取得できるようになります。
// searchParamsのschemaを作成
import { parseAsString } from "nuqs";
import { createSearchParamsCache } from "nuqs/parsers";
export const searchParamsCache = createSearchParamsCache({
search: parseAsString.withDefault(""),
});
// ページルートでキャッシュ
export default function Page({
searchParams,
}: { searchParams: Record<string, string | string[] | undefined> }) {
searchParamsCache.parse(searchParams);
return (
<div>
<NestServer />
</div>
);
}
// 末端のserver componentでも取得できる
export const NestServer = () => {
const searchParams = searchParamsCache.all();
return <div>{searchParams.search}</div>;
};
まとめ
いかがでしたか?nuqsを使えば、URLSearchParamsの型安全性の問題を解決できるだけでなく、開発効率も大幅に向上します。テスト体験やサーバー側でのデータ検証など、まだ改善の余地はありますが、nuqsは検索パラメータの扱いを劇的に改善してくれます。
型安全な世界を目指して、nuqsを使ってみませんか?
Discussion