0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-02-09

はじめに

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

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

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

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

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

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

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

/invoices/create/page.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


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

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

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

image.png


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

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

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

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

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

/aap/lib/actions.ts
'use server'

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

/aap/lib/actions.ts
'use server'

export async function createInvoice(formData: FormData) {}

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

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

/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.tscreateInvoiceを呼び出します。

その際にformDataを受け取ることができます。

作業がしやすいようにrawFromDataというオブジェクトの形にしておきましょう。

/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を出力してみると以下のようになります。

コンソール
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については別の記事に記載したのでそちらをご覧ください。

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

/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はセント単位で保存するらしいので

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

/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;
}

datestringで受け取りましたが、データベースに保存する際は日付型なので、こちらも変換しましょう。

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

/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関数を使用しています。

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

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

次の記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?