Server Components で複数選択できるフィルタリングを実装する
Web ページ上にフィルタリング機能を実装したい場面は頻繁に遭遇します。例えば、ブログの記事一覧ページにて複数個のタグを選択し、その状態に応じて表示する記事を変えるなどです。
やりたいこと(https://いなにわうどん.みんな/articles で使用)
こうした場合に、従来の React では選択中のタグを useState 等の hooks に持たせ、その内容に応じてフィルタリングした結果を useMemo 等に記憶させることが一般的でした。しかしながら、Next.js 13 から標準で採用されている React Server Components(RSC)では状態や副作用を持つことができないため、新たな状態管理の手法を考える必要があります。
解決策
URL 中のクエリパラメータを利用してタグの選択を表現します。例えば、パラメータ名を tag
とするとき、パラメータと状態の紐づけとして以下を考えます。
-
?
:タグが未選択 -
?tag=foo
:タグ foo のみが選択されている -
?tag=foo+bar
:タグ foo, bar が複数選択されている
next/link を用いてこれらのパラメータを付与しながら遷移することで、クライアントでの状態管理に依存することなく、フィルタリング機能が実装できます。JS が無効な環境からのアクセスでも動作する点もポイントです[1]。
実装
useRouter に代わって、Next.13 では NextPage の props として searchParams が取得できます[2]。
interface PageProps {
searchParams: { [key in string]: string | string[] | undefined }
}
searchParams が string 型であった場合に限って、デリミタ(+
)で分割した値を選択中のタグとして利用します。string 型の場合は初期値(今回の場合は空配列)としておきます。
import CheckBox from "./CheckBox";
// コンテンツ
type Item = { tag: string; content: string; }
const items: Item[] = [
{ tag: "tech", content: "技術" },
{ tag: "random", content: "雑談" },
{ tag: "tech", content: "技術 2(ツー)" },
{ tag: "tech", content: "雑談 2(ツー)" },
];
const tags = ["tech", "random"];
const Page = ({ searchParams }: PageProps) => {
const tagParams = searchParams["tag"];
const selectedTags = typeof tagParams === "string" ? tagParams.split("+") : [];
const filteredItems = items.filter(item => selectedTags.includes(item.tag));
return (
<div>
<CheckBox paramKey="tag" tags={tags} searchParams={searchParams} />
<ul>
{filteredItems.map((item) => (
<li key={item.content}>{item.content}</li>
))}
</ul>
</div>
);
}
export default Page;
複数選択できるタグ
タグの選択を実現するコンポーネントは以下の実装になります。
URL の履歴にパラメータの変遷を残したくない場合は、Link コンポートに replace
属性を指定します。
import Link from 'next/link';
interface CheckboxProps {
paramKey: string;
tags: string[];
searchParams: { [key in string]: string | string[] | undefined };
}
const Checkbox = ({ paramKey, tags, searchParams }: CheckboxProps) => {
// タグが選択中であるか
const isSelected = (tag: string) => {
const param = searchParams[paramKey];
return typeof param === 'string' && param.split('+').includes(tag);
};
// タグの選択を追加 or 解除する
const getNewParams = (tag: string) => {
// string 型であるパラメータのみを抽出
const newParams: { [key in string]: string } = {};
for (const [key, value] of Object.entries(searchParams)) {
if (typeof value === 'string') {
newParams[key] = value;
}
}
if (!newParams[paramKey]) {
newParams[paramKey] = '';
}
const oldTags = newParams[paramKey].split('+');
const newKeys = oldTags.includes(tag)
? oldTags.filter((key) => key !== tag)
: [...oldTags, tag];
newParams[paramKey] = newKeys.join('+');
return new URLSearchParams(newParams);
};
return (
<ul>
{tags.map((tag) => (
<li key={tag}>
<Link href={`?${getNewParams(tag)}`}>
{tag} {isSelected(tag) && '選択中'}
</Link>
</li>
))}
</ul>
);
};
export default Checkbox;
-
Server Actions では、JS が無効でも動作する機能を Progressive Enhancement と呼称している ↩︎
-
useSearchParams を使用する方法もありますが、これは Client Components 専用です ↩︎
Discussion