【12-1】Next.js app routerのチュートリアルやってみる(React Server Actionsを使ったデータ作成処理)

# はじめに

Next.js app routerのチュートリアルの第12-1章のアウトプットをします。

https://nextjs.org/learn/dashboard-app/mutating-data

<details><summary>前の記事</summary>

【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 パラメータを利用したページネーション)

https://qiita.com/naoyuki2/items/fd00dc2b376e7d87fb44

</details>

# 第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`という記述が必要になります。

```diff_tsx:example
// 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. ユーザーの入力を取得するフォームを作成
1. サーバーアクションを作成し、フォームから呼び出す
1. データを抽出する
1. データベースに挿入するデータの型を検証し、準備する
1. データを挿入
1. 再検証とリダイレクト

## 1. ユーザーの入力を取得するフォームを作成

フォームは`/invoices/create/page.tsx`に実装します。

 ```
 .
└── invoices/
    ├── create/
    │   └── page.tsx // 新しい請求書を作成するページ
    └── page.tsx // 請求書一覧を表示するページ
```

`/invoices/create/page.tsx`を作成したら、以下を貼り付けましょう。

```diff_tsx:/dashboard/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 />`はパンくずリストのことですね。

すでに作成されています。

今回は特に触れません。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/267b9230-e8a0-641d-3f5c-a2b22423661a.png)

---

`<Form />`コンポーネントもすでに作成されています。

`fetchCustomers()`で顧客の情報を取得し、`<Form />`コンポーネントに渡しています。

請求書作成時に、下のようにプルダウンで顧客の名前を選択するためです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/a4c9b6eb-670a-3907-2d3e-6152bb2bcaf8.png)

---

## 2. サーバーアクションを作成し、フォームから呼び出す

フォームの送信時に呼び出されるサーバー アクションを作成しましょう。

`lib`ディレクトリに移動し、`actions.ts`というファイルを作りましょう。

このファイルにサーバーアクションを記述していくため、１行目に`use server`をつけておきましょう。

これをつけることで、`actions.ts`内に書いた関数は全てサーバーアクションと認識されます。

```tsx:/aap/lib/actions.ts
'use server'
```

`createInvoice`関数を定義しましょう。

```tsx:/aap/lib/actions.ts
'use server'

export async function createInvoice(formData: FormData) {}
```

とりあえず、`form`側で`createInvoice`関数の`import`だけしておきましょう。

そして、`form`タグの`action`属性に`createInvoice`を指定することで、フォーム送信時に発火できるようになります。

```diff_tsx:/app/ui/invoices/create-form.tsx
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`というオブジェクトの形にしておきましょう。

```diff_tsx:/app/lib/actions.ts
'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`を出力してみると以下のようになります。

```shell:コンソール
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')`

で取得することができます。

詳しくは下を見てください。

https://developer.mozilla.org/ja/docs/Web/API/FormData

# 4. データベースに挿入するデータの型を検証し、準備する

フォームをデータベースに送信する前に、データが正しい形式と正しい型であるかを確認する必要があります。

その検証のために`Zod`というライブラリを使用しました。

`Zod`については別の記事に記載したのでそちらをご覧ください。

https://qiita.com/naoyuki2/items/b64dadd21d86facb5375

型の検証が終わって、それぞれのデータを分割代入で受け取ることができました。

```tsx:/app/lib/actions.ts
// ...
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`はセント単位で保存するらしいので

金額をセントに変換します。

```diff_tsx:/app/lib/actions.ts
// ...
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`で受け取りましたが、データベースに保存する際は日付型なので、こちらも変換しましょう。

```diff_tsx:/app/lib/actions.ts
// ...
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 クエリを作成して新しい請求書をデータベースに挿入しましょう。

```diff_tsx:/app/lib/actions.ts
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`関数を使用しています。

これは古いキャッシュでデータを読み込むのではなく、新しいデータをサーバーから取得するために必要です。

```diff_tsx:
'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処理)です。

# 次の記事

https://qiita.com/naoyuki2/items/41f16ef69a50171d9d86