はじめに
Next.js app routerのチュートリアルの第12-1章のアウトプットをします。
前の記事
【01】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/af58da3d20cbc790e767
【02】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/edf450b3ee135e83d1e8
【03】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/612221eac233aa9cbb74
【04】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/62f9beccbfe36eaf7f90
【05】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/8b71b1d1df7c9435a9c9
【06】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/58130c3cfbaf8a573de2
【07】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/2c2da0f8071e60454679
【08】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/45f45fcb9cc14506f79f
【09】Next.js app routerのチュートリアルやってみる(loading.tsxとSuspenseでストリーミング)
https://qiita.com/naoyuki2/items/717694288ec6017a3af2
【10】Next.js app routerのチュートリアルやってみる(部分的な事前レンダリング)
https://qiita.com/naoyuki2/items/8062f755b0679fe925b1
【11-1】Next.js app routerのチュートリアルやってみる(URLパラメーターを利用した検索機能)
https://qiita.com/naoyuki2/items/2be9503ac80fc4a1fa6a
【11-2】Next.js app routerのチュートリアルやってみる(URL パラメータを利用したページネーション)
第12-1章 データの変更
この章では以下を学びました。
- React Server Actions
- データの作成(Create)
React Server Actions
React Server Actions
を使用すると、サーバー上で非同期コードを直接実行できます。また、データを変更するために
API エンドポイント
を作成する必要がなくなります代わりに、サーバー上で実行され、クライアント コンポーネントまたはサーバー コンポーネントから呼び出すことができる非同期関数を作成します。
React Server ActionsでのFormの使用方法
今回は、<form />
タグを使ってデータを入力し送信します。
<form />
タグのaction
属性に実行したいサーバーアクションを代入しておくことで、
フォームイベント発火時にサーバーアクションを動作させることができます。
また、React Server Actions
を利用する際は、use server
という記述が必要になります。
// Server Component
export default function Page() {
// Action
async function create(formData: FormData) {
'use server';
// Logic to mutate data...
}
// Invoke the action using the "action" attribute
return <form action={create}>...</form>;
}
従来であればフォームはクライアントコンポーネントで実装する必要がありました。
(input
要素のonChange
イベントやform
要素のonSubmit
などはクライアントコンポーネントでしか動作しないため)
ですが、React Server Actions
の登場により、サーバーコンポーネントで実装できるようになりました。
クライアントで JavaScript が無効になっている場合でもフォームは機能します。
また、サーバー アクションを通じてフォームが送信されると、そのアクションを使用してデータを変更できるだけでなく、revalidatePath や revalidateTag などの API を使用して関連するキャッシュを再検証することもできます。
データの作成(Create)
データの作成は以下の手順で実装します。
- ユーザーの入力を取得するフォームを作成
- サーバーアクションを作成し、フォームから呼び出す
- データを抽出する
- データベースに挿入するデータの型を検証し、準備する
- データを挿入
- 再検証とリダイレクト
1. ユーザーの入力を取得するフォームを作成
フォームは/invoices/create/page.tsx
に実装します。
.
└── invoices/
├── create/
│ └── page.tsx // 新しい請求書を作成するページ
└── page.tsx // 請求書一覧を表示するページ
/invoices/create/page.tsx
を作成したら、以下を貼り付けましょう。
import Form from '@/app/ui/invoices/create-form';
import Breadcrumbs from '@/app/ui/invoices/breadcrumbs';
import { fetchCustomers } from '@/app/lib/data';
export default async function Page() {
const customers = await fetchCustomers();
return (
<main>
<Breadcrumbs
breadcrumbs={[
{ label: 'Invoices', href: '/dashboard/invoices' },
{
label: 'Create Invoice',
href: '/dashboard/invoices/create',
active: true,
},
]}
/>
<Form customers={customers} />
</main>
);
}
<Breadcrumbs />
はパンくずリストのことですね。
すでに作成されています。
今回は特に触れません。
<Form />
コンポーネントもすでに作成されています。
fetchCustomers()
で顧客の情報を取得し、<Form />
コンポーネントに渡しています。
請求書作成時に、下のようにプルダウンで顧客の名前を選択するためです。
2. サーバーアクションを作成し、フォームから呼び出す
フォームの送信時に呼び出されるサーバー アクションを作成しましょう。
lib
ディレクトリに移動し、actions.ts
というファイルを作りましょう。
このファイルにサーバーアクションを記述していくため、1行目にuse server
をつけておきましょう。
これをつけることで、actions.ts
内に書いた関数は全てサーバーアクションと認識されます。
'use server'
createInvoice
関数を定義しましょう。
'use server'
export async function createInvoice(formData: FormData) {}
とりあえず、form
側でcreateInvoice
関数のimport
だけしておきましょう。
そして、form
タグのaction
属性にcreateInvoice
を指定することで、フォーム送信時に発火できるようになります。
import { customerField } from '@/app/lib/definitions';
import Link from 'next/link';
import {
CheckIcon,
ClockIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '@/app/ui/button';
+import { createInvoice } from '@/app/lib/actions';
export default function Form({
customers,
}: {
customers: customerField[];
}) {
return (
+ <form action={createInvoice}>
// ...
)
}
知っておくべきこと
HTMLでは、
<form />
タグのaction
属性にURL
を渡します。渡すURLは、フォーム データの送信先 (通常は API エンドポイント) になります。ただし、React では、このaction属性は特別な prop とみなされます。つまり、React はその上に構築され、アクションを呼び出すことができるようになります。
サーバー アクションは舞台裏でPOST API エンドポイントを作成します。これが、サーバー アクションを使用するときに API エンドポイントを手動で作成する必要がない理由です。
3. データを抽出する
create-form.tsx
からactions.ts
のcreateInvoice
を呼び出します。
その際にformData
を受け取ることができます。
作業がしやすいようにrawFromData
というオブジェクトの形にしておきましょう。
'use server';
export async function createInvoice(formData: FormData) {
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
};
// Test it out:
console.log(rawFormData);
}
フォーム側でcreateInvoice
を呼び出す際に引数を指定していませんが、
通常、フォーム送信の際にはイベントオブジェクトが自動的
に引数として渡されます。
このイベントオブジェクトを使用して、フォーム内の各 input
要素の値を取得することができます。
コンソールにformData
を出力してみると以下のようになります。
FormData {
[Symbol(state)]: [
{
name: 'customerId',
value: '3958dc9e-737f-4377-85e9-fec4b6a6442a'
},
{ name: 'amount', value: '12' },
{ name: 'status', value: 'paid' }
]
}
<form />
タグ内には以下の要素があります。
- nameに
customerId
を持った<select />
タグ - nameに
amout
を持った<input />
タグ - nameに
status
を持った<input />
タグ
例えばcustomerId
を取得したい場合は、
formData.get('customerId')
で取得することができます。
詳しくは下を見てください。
4. データベースに挿入するデータの型を検証し、準備する
フォームをデータベースに送信する前に、データが正しい形式と正しい型であるかを確認する必要があります。
その検証のためにZod
というライブラリを使用しました。
Zod
については別の記事に記載したのでそちらをご覧ください。
型の検証が終わって、それぞれのデータを分割代入で受け取ることができました。
// ...
export async function createInvoice(formData: FormData) {
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
}
amout
はセント単位で保存するらしいので
金額をセントに変換します。
// ...
export async function createInvoice(formData: FormData) {
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
+ const amountInCents = amount * 100;
}
date
はstring
で受け取りましたが、データベースに保存する際は日付型なので、こちらも変換しましょう。
// ...
export async function createInvoice(formData: FormData) {
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
const amountInCents = amount * 100;
+ const date = new Date().toISOString().split('T')[0];
}
5. データを挿入
データベースに必要な値がすべて揃ったので、SQL クエリを作成して新しい請求書をデータベースに挿入しましょう。
import { z } from 'zod';
+import { sql } from '@vercel/postgres';
// ...
export async function createInvoice(formData: FormData) {
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
+ await sql`
+ INSERT INTO invoices (customer_id, amount, status, date)
+ VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
+ `;
}
エラーハンドリングは第13章で行います。
6. 再検証とリダイレクト
データを作成した後に、請求書一覧ページにリダイレクトさせます。
そのためには、redirect
関数を使用します。
ですが、その前にrevalidatePath
関数を使用しています。
これは古いキャッシュでデータを読み込むのではなく、新しいデータをサーバーから取得するために必要です。
'use server';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
+import { revalidatePath } from 'next/cache';
+import { redirect } from 'next/navigation';
// ...
export async function createInvoice(formData: FormData) {
const { customerId, amount, status } = CreateInvoice.parse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
+ revalidatePath('/dashboard/invoices');
+ redirect('/dashboard/invoices');
}
revalidatePath
がない場合は、ページ遷移は高速になるかも知れませんが、古いキャッシュからデータを読み込むことになるため、作成したデータが表示されない可能性があります。
おわりに
作成だけでもなかなか時間がかかってしまった。
次回はデータの更新(Update処理)です。
次の記事