🔖

Next.js サーバーアクション入門:Hooks不要でフォーム送信がシンプルに

に公開

この記事の対象者

・サーバーアクションを使ったことがない人
・Next.js初心者〜中級者
・実務の導入を考えていて、概要をざっくり知りたい方

サーバーアクションとは

サーバーアクションとは、サーバー上で実行される非同期関数です。これはサーバーコンポーネントとクライアントコンポーネントの両方で呼び出すことができ、Next.jsアプリケーションでフォーム送信とデータミューテーションを処理します。
従来のNext.jsやReactアプリケーションでは、ボタンクリックなどのUIイベントからデータを更新するためには、APIエンドポイント(API Routesもしくはroute handlerなど)を作成し、クライアント側からそのAPIを呼び出すという手順が必要でした。サーバーアクションは、これらの手順を簡略化し、サーバー側での処理とフロントエンドのイベントハンドリングを直接統合することで、コード量を削減し開発効率を向上させます。

サーバーアクションにするには?

use serverを付ける

サーバーアクションにするには、下記のコードのように関数の本体の冒頭に'use server';を付ける必要があります。

async function addToCart(data) {
  'use server';
  // ...
}

また、個々の関数に use serverをマークする代わりに、このディレクティブをファイルの先頭に追加することもできます。その場合はそのファイル内のすべてのエクスポートが、クライアントコードでインポートされる場合も含み、サーバーアクションとしてマークされます。

非同期関数でないといけない

サーバーアクションは非同期関数でしか使えません。

サーバー側でのみ定義されていること

サーバーコンポーネント内、またはサーバー専用のファイルで定義する必要があります。
`use client'のクライアントコンポーネント内には直接書けません。
もしクライアントから呼びたい場合は、サーバーアクションをサーバー専用のファイルで定義し、それをクライアントコンポーネントにimportして直接呼び出すか、サーバーアクションをpropsとして渡すようにします。

インラインで使える

サーバーアクションは以下のコード例のようにインラインでも使えます。
しかし、再利用できなかったり、コードが読みにくかったりとあまり推奨はされません。

// app/page.tsx
export default function Page() {
  return (
    <form
      action={async (formData) => {
        "use server";
        const name = formData.get("name") as string;
      }}
    >
      <input type="text" name="name" placeholder="名前を入力" />
      <button type="submit">送信</button>
    </form>
  );
}

簡単な例

getMessage.js
// src\app\getMessage.js
"use server";

export async function getMessage() {
  return "Hello World";
}
page.jsx
// src\app\serverActionsClient\page.jsx
"use client"

import { useEffect, useState } from "react"
import { getMessage } from "../getMessage"


const ClientExample = () => {

  const [message, setMessage] = useState('')
 
  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await getMessage()
      setMessage(updatedViews)
    }
 
    updateViews()
  }, [])

  return (
    <div>
      <p>{message}</p>
    </div>
  )
}
 
export default ClientExample;
page.jsx
// src\app\serverActionsServer\page.jsx
import { getMessage } from "../getMessage";

const ServerExample = async () => {
  const message = await getMessage();

  return (
    <div>
      <p>{message}</p>
    </div>
  );
};

export default ServerExample;

"use server"がある/ないでどう違うのか?

クライアントコンポーネントから"use server"ありの関数となしの関数を呼び出してみます。

"use server";

export async function serverAction() {
  console.log("Hello from Server Action");
  return "done (server)";
}
export async function clientFunc() {
  console.log("Hello from Client Func");
  return "done (client)";
}
"use client";

import { useEffect } from "react";
import { serverAction } from "../action/page";
import { clientFunc } from "../client/page";

export default function ClientPage() {
  useEffect(() => {
    serverAction(); // サーバーにリクエスト → ターミナルにログ
    clientFunc(); // クライアントで実行 → ブラウザにログ
  }, []);

  return <p>Check console logs</p>;
}

use serverありのほうではターミナルでconsoleが表示されているのに対し、use serverなしのほうではブラウザ側にconsoleが表示されるのがわかると思います。
つまりuse serverありではNode.js環境で動いていることがわかります。

ちなみに上記のコード例では、use clientファイルからでも、サーバーで定義したサーバーアクションをインポートして呼び出せば、裏でRPCが走りサーバー側で実行されます。

ただし、use clientファイル内で use server"を書いてサーバーアクションを定義することはできない点に注意してください。
サーバーアクションは「サーバーで定義し、サーバーコンポーネントまたはクライアントコンポーネントから呼び出す」ことができるのです。

メリット

サーバーアクションのメリットは何でしょうか?
以下の通りです。

hooksを使わなくてよく、やりとりの回数が減るのでパフォーマンスも上がる。

サーバーコンポーネントやフォームでサーバーアクションを呼び出す場合、複雑なhooksを使わずに済むため、コード量が減り可読性が向上します。
※クライアントコンポーネントから呼び出す場合は、useState や useEffect が必要です。

また、従来は「クライアントでフォーム送信 →fetch→route handler→DB」という形で「API経由の通信」が必要でしたが、サーバーアクションなら<form action={serverAction}>と書くだけで 「クライアント→サーバー関数直結」と、route handlerを自前で用意する必要がなくなり、通信経路がシンプルになり、無駄なオーバーヘッドが減るのでパフォーマンスも上げることができるのです。

ハイドレーションが完了する前に実行できる(プログレッシブエンハンスメント)

こちらはメリットの中でも影響がそれほど大きくないので、サラッと紹介します。
サーバーアクションのフォーム送信は、React のハイドレーションが完了する前から利用できます。
これは「プログレッシブエンハンスメント」と呼ばれる考え方で、JavaScriptがまだ動作していない環境でもフォーム送信を保証できる仕組みです。
影響は大きくありませんが、ユーザー体験の安定性やアクセシビリティの面で意義があります。

デメリット

デメリットをさらっと上げると、新しい技術なのでそれほど情報が出回っていないことです。
Next.js 13/14から追加された比較的新しい仕組みのため、まだ仕様変更が入りやすく、エコシステムやライブラリの対応が不十分なこともあります。

また、サーバーサイドとクライアントサイドの境界が曖昧になるため、どこで処理が実行されるのかがわかりづらく、担当領域が不明瞭になることがあります。

具体例

最後にサーバーアクションを使った具体的なケースを見ていきます。
代表的なのはフォームでの実装です。

"use server";

export async function sendMessage(formData) {
  const message = formData.get("message");
  console.log(message);
}
import React from "react";
import { sendMessage } from "../sendMessage";

const page = () => {
  return (
    <div>
      <form action={sendMessage}>
        <label htmlFor="message">メッセージ</label>
        <input type="text" name="message" />
        <button type="submit">送信</button>
      </form>
    </div>
  );
};

export default page;

上記ではフォームのaction属性にサーバーアクションであるsendMessage関数をimportして使用しています。
また、sendMessage関数では引数にformDataを記述し、 const message = formData.get("message");のように記述することで指定したname属性の値を取得することができます。
実際の画面で「こんにちは」と送信すると、ターミナル上で送信されていることが確認できます。


最後に

今回は今更ながらサーバーアクションを取り上げてみました。
割と最新目な情報なので学習を後回しにしていました。
現在担当している案件ではフォームを実装しているのですが、従来のhooksを使って実装しており、サーバーアクションを学習する機会がありませんでした。
将来的にフォーム実装をリードするならサーバーアクションもありだなと思い、重い腰を持ち上げて学習し、ブログにアウトプットしてみた次第です。
今回は案件で使う予定がないので、サーバーアクションに関しての基本しか取り上げませんでした。
次回はroute handlerに関して取り上げようと思います。

参考
Next.js Server Actions Simply Explained in just 5 Minutes(Tobi Mey)
あなたは"use client"と"use server"をいつ使うべきか知っていますか?【ServerActionsまで解説】(プログラミングチュートリアル)

Discussion