【Next.js, Prisma】nuqs を使って URL クエリパラメータを活用した動的検索を作りたい
はじめに
管理画面で大量のデータを扱う場合、検索フォームは必須ですよね。
最近、検索フォームの実装に nuqs を使用して大変便利に感じましたので、Next.js、Prisma、そして nuqs を組み合わせて、URL クエリパラメータを活用した動的検索機能の実装方法を紹介します。
使用する package
- nuqs
- Next.js
- Prisma
- React Hook Form
- Shadcn UI
作ったもの
今回は簡単な例として、ユーザーの名前とメールアドレスそれぞれの部分一致で絞り込み可能な一覧画面を実装します。
name と email に値を入れなかった場合は、全ユーザーを表示します。
name もしくは email に値をいれ、submit すると部分一致するユーザーのみを絞り込み表示できます。
name と email で AND 検索も可能です。
実装のポイント
page の実装
import FilterForm from '@/components/filter-form';
import Users, { type UsersProps } from '@/components/users';
import prisma from '@/lib/prisma';
import type { Prisma } from '@prisma/client';
import { createSearchParamsCache, parseAsString, type SearchParams } from 'nuqs/server';
import type { JSX } from 'react';
const searchParamsCache = createSearchParamsCache({
name: parseAsString,
email: parseAsString,
});
type Response<T> = {
data?: T;
error?: string;
};
async function fetchUsers({
name,
email,
}: {
name?: string | null;
email?: string | null;
}): Promise<Response<UsersProps['users']>> {
try {
const whereConditions: Prisma.UserWhereInput = {};
if (name) {
whereConditions.name = { contains: name };
}
if (email) {
whereConditions.email = { contains: email };
}
const users = await prisma.user.findMany({
where: whereConditions,
select: {
id: true,
name: true,
email: true,
},
});
return {
data: users,
};
} catch (error) {
console.error(error);
return {
error: 'ユーザーの取得に失敗しました',
};
}
}
type PageProps = {
searchParams: SearchParams;
};
export default async function Page({ searchParams }: PageProps): Promise<JSX.Element> {
const query = await searchParamsCache.parse(searchParams);
const { data: users, error } = await fetchUsers({ ...query });
if (error) {
return <div>{error}</div>;
}
if (!users) {
return <div>ユーザーが見つかりませんでした</div>;
}
return (
<>
<FilterForm />
<Users users={users} />
</>
);
}
fetchUsers 関数は prisma から user を一覧取得する関数です。name と email を引数に受け取り、where 句にセットさせることで絞り込みをかけます。
const whereConditions: Prisma.UserWhereInput = {};
if (name) {
whereConditions.name = { contains: name };
}
if (email) {
whereConditions.email = { contains: email };
}
const users = await prisma.user.findMany({
where: whereConditions,
select: {
id: true,
name: true,
email: true,
},
});
return {
data: users,
};
検索フォームの実装
'use client';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useQueryState } from 'nuqs';
import type { JSX } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const formSchema = z.object({
name: z.string().optional(),
email: z.string().optional(),
});
type FormValues = z.infer<typeof formSchema>;
export default function FilterForm(): JSX.Element {
const router = useRouter();
const [name, setName] = useQueryState('name', {
history: 'push',
defaultValue: '',
});
const [email, setEmail] = useQueryState('email', {
history: 'push',
defaultValue: '',
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: name || '',
email: email || '',
},
});
const onSubmit = async (values: FormValues) => {
await Promise.all([setName(values.name || null), setEmail(values.email || null)]);
router.refresh();
};
const onReset = async (): Promise<void> => {
form.reset({ name: '', email: '' });
await setName(null);
await setEmail(null);
router.push('/users');
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex items-end space-x-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Filter</Button>
<Button type="button" variant="outline" onClick={onReset}>
Reset
</Button>
</form>
</Form>
);
}
nuqs では以下のような書きぶりで、クエリパラメータを扱うことができます。とてもシンプルです。
const [name, setName] = useQueryState('name', {
history: 'push',
defaultValue: '',
});
const [email, setEmail] = useQueryState('email', {
history: 'push',
defaultValue: '',
});
フィルター実行時の onSubmit は以下のようになります。
useQueryState の変更関数で入力値を state にセットし、router.refresh()
で page.tsx(RSC)を再レンダリングさせます。これにより、クエリパラメータで動的に絞り込みが可能になります。
const onSubmit = async (values: FormValues) => {
await Promise.all([setName(values.name || null), setEmail(values.email || null)]);
router.refresh();
};
値のリセットは以下のようになります。こちらも同様、クエリパラメータを削除後は、page.tsx で絞り込みされていない状態のデータを再取得したいので、router.push
を実行しています。
const onReset = async (): Promise<void> => {
form.reset({ name: '', email: '' });
await setName(null);
await setEmail(null);
router.push('/users');
};
処理の流れ
- FilterForm の onSubmit 実行時に URL パラメータが追加され、page.tsx が再レンダリングされる
- page.tsx の
fetchUsers
関数に query 文字列が渡され、user の検索結果を取得する - Users コンポーネントに検索結果を渡して描画する
まとめ
本記事では、Next.js、Prisma、そしてnuqsを組み合わせて、URLクエリパラメータを活用した動的検索機能の実装方法を紹介しました。
今回はユーザー名とメールアドレスによる部分一致検索を例に挙げましたが、この手法は様々な検索条件や複雑なフィルタリングにも応用可能です。
今後の発展としては、ページネーション機能の追加、より複雑な検索条件の実装、パフォーマンスの最適化などが考えられます。
今回はテーブルの実装をご紹介できなかったので、今度紹介します。
このアプローチを基に、プロダクトの要件に合わせてカスタマイズしていってみてくださいね。
Discussion