Closed4

Next.jsの理解を深める!Chapter 12

nakamotonakamoto

Chapter 12(first)

What are Server Actions?
React Server Actions allow you to run asynchronous code directly on the server. They eliminate the need to create API endpoints to mutate your data. Instead, you write asynchronous functions that execute on the server and can be invoked from your Client or Server Components.

Security is a top priority for web applications, as they can be vulnerable to various threats. This is where Server Actions come in. They offer an effective security solution, protecting against different types of attacks, securing your data, and ensuring authorized access. Server Actions achieve this through techniques like POST requests, encrypted closures, strict input checks, error message hashing, and host restrictions, all working together to significantly enhance your app's safety.

  • React Server Actionsはサーバー上で非同期コードを直接実行することを可能にする。その為、データを変更するためのAPIエンドポイントを作成する必要がなくなる。代わりにサーバーで実行される非同期関数を記述し、クライアントまたはサーバーコンポーネントから呼び出せる。

Using forms with Server Actions
In React, you can use the action attribute in the <form> element to invoke actions. The action will automatically receive the native FormData object, containing the captured data.

An advantage of invoking a Server Action within a Server Component is progressive enhancement - forms work even if JavaScript is disabled on the client.

// 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>;
}
  • <form> 要素のaction属性を使用してServer Actionsを呼び出せる。このアクションは自動的に、フォームで取得したデータを含むネイティブのFormDataオブジェクトを受け取る。
  • クライアント側でJavaScriptが無効になっていてもフォームは機能する。

Next.js with Server Actions
Server Actions are also deeply integrated with Next.js caching. When a form is submitted through a Server Action, not only can you use the action to mutate data, but you can also revalidate the associated cache using APIs like revalidatePath and revalidateTag.

  • Server ActionsはNext.jsのキャッシングと深く統合される。フォームがServer Actionを通じて送信されると、データを変更するだけでなく、revalidatePathrevalidateTag等のAPIを使用して関連するキャッシュを再検証できる。これにより、データの変更があった場合に、キャッシュされたコンテンツを効率的に最新の状態に保つことが可能になる。

Create a Server Action
Great, now let's create a Server Action that is going to be called when the form is submitted.

Navigate to your lib directory and create a new file named actions.ts. At the top of this file, add the React use server directive:

/app/lib/actions.ts
'use server';

By adding the 'use server', you mark all the exported functions within the file as server functions. These server functions can then be imported into Client and Server components, making them extremely versatile.

You can also write Server Actions directly inside Server Components by adding "use server" inside the action. But for this course, we'll keep them all organized in a separate file.

  • use serverを追加することによってファイル内の全てのエクスポートされた関数をサーバー関数として定義する。これらのサーバー関数はクライアントコンポーネントやサーバーコンポーネントにインポートでき、非常に汎用性が高くなる。また、アクション内にuse serverを追加するとサーバーコンポーネント内で直接Server Actionsを記述できる。

Extract the data from formData
Back in your actions.ts file, you'll need to extract the values of formData, there are a couple of methods you can use. For this example, let's use the .get(name) method.

/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);
}
  • formDataから値を抽出するには.get(name) メソッドを使用できる。
    .get(name) メソッドを使うと指定した名前のフォームデータの値を取得できる。

Validate and prepare the data
Before sending the form data to your database, you want to ensure it's in the correct format and with the correct types.

To handle type validation, you have a few options. While you can manually validate types, using a type validation library can save you time and effort. For your example, we'll use Zod, a TypeScript-first validation library that can simplify this task for you.

In your actions.ts file, import Zod and define a schema that matches the shape of your form object. This schema will validate the formData before saving it to a database.

/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
 
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});
 
const CreateInvoice = FormSchema.omit({ id: true, date: true });
 
export async function createInvoice(formData: FormData) {
  // ...
}

The amount field is specifically set to coerce (change) from a string to a number while also validating its type.

You can then pass your rawFormData to CreateInvoice to validate the types:

/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'),
  });
}
  • フォームデータをDBに送信する前にその形式や型が正しいことを確認する必要がある。型の検証にはいくつかの方法があるが、手動で型を検証する代わりに、型検証ライブラリを使用すると時間と労力を節約できる。例として、TypeScriptに特化した検証ライブラリであるZodを使用する。特にamount フィールドは文字列から数値への強制変換を行いながら、その型も検証される。
nakamotonakamoto

Chapter 12(second)

Revalidate and redirect
Next.js has a Client-side Router Cache that stores the route segments in the user's browser for a time. Along with prefetching, this cache ensures that users can quickly navigate between routes while reducing the number of requests made to the server.

Since you're updating the data displayed in the invoices route, you want to clear this cache and trigger a new request to the server. You can do this with the revalidatePath function from Next.js:

/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
 
// ...
 
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');
}

Once the database has been updated, the /dashboard/invoices path will be revalidated, and fresh data will be fetched from the server.

At this point, you also want to redirect the user back to the /dashboard/invoices page. You can do this with the redirect function from Next.js:

/app/lib/actions.ts
'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) {
  // ...
 
  revalidatePath('/dashboard/invoices');
  redirect('/dashboard/invoices');
}
  • Next.jsでは、クライアントサイドのルーターキャッシュがあり、これによりユーザーのブラウザにルートセグメントが一定期間保存される。これにより、プリフェッチングと組み合わせてユーザーがルート間を迅速に移動できるようにしながらサーバーへのリクエスト数を減らす。請求書ルートに表示されるデータを更新するため、このキャッシュをクリアし、サーバーへの新しいリクエストをトリガーするにはrevalidatePath関数を使う。DBが更新されたら/dashboard/invoicesパスは再検証され、サーバーから新しいデータが取得される。この時点で、ユーザーを/dashboard/invoicesページにリダイレクトさせるにはredirect関数を使う。

Updating an invoice
The updating invoice form is similar to the create an invoice form, except you'll need to pass the invoice id to update the record in your database. Let's see how you can get and pass the invoice id.

These are the steps you'll take to update an invoice:

Create a new dynamic route segment with the invoice id.
Read the invoice id from the page params.
Fetch the specific invoice from your database.
Pre-populate the form with the invoice data.
Update the invoice data in your database.

Pass the id to the Server Action
Lastly, you want to pass the id to the Server Action so you can update the right record in your database. You cannot pass the id as an argument like so:

/app/ui/invoices/edit-form.tsx
// Passing an id as argument won't work
<form action={updateInvoice(id)}>

Instead, you can pass id to the Server Action using JS bind. This will ensure that any values passed to the Server Action are encoded.

/app/ui/invoices/edit-form.tsx
// ...
import { updateInvoice } from '@/app/lib/actions';
 
export default function EditInvoiceForm({
  invoice,
  customers,
}: {
  invoice: InvoiceForm;
  customers: CustomerField[];
}) {
  const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
 
  return (
    <form action={updateInvoiceWithId}>
      <input type="hidden" name="id" value={invoice.id} />
    </form>
  );
}
  • Reactの<form>要素でaction属性を使用する場合、直接updateInvoice(id)のような関数を呼び出せない。なぜなら、action属性はフォームが送信される際に実行されるべき関数を指定するものであり、コンポーネントがレンダリングされる際にその関数を実行してしまうため。代わりに、JavaScriptのbindメソッドを使って、updateInvoice関数にidを事前に束縛する。これにより、updateInvoiceWithIdupdateInvoice関数の新しいバージョンとなり、指定されたidを引数として含んでいる。フォームが送信される際には、このupdateInvoiceWithId関数が適切なidと共に呼び出される。
このスクラップは3ヶ月前にクローズされました