Server Functions VS Actions【Next.js】
はじめに
先日、Next.js の勉強会で、サーバーサイドの処理について取り上げました 📝
サーバーサイドの処理は、モダンな Web アプリケーション開発において重要でありながら、
少しややこしい概念が多く、理解するのが難しい要素です。。
今回は、Next.js のサーバーファンクションとアクションについて調査したので、基礎的な内容をまとめました!
時間の節約になれば、嬉しいです 🙌
TL;DR
-
サーバーファンクションは、:
- サーバー側で実行される非同期関数
-
"use server"
ディレクティブで定義 - クライアントコンポーネントから呼び出し可能
-
アクションは、:
- The async transition(非同期遷移)を使用する関数
- フォーム送信やデータ変更に特化
- ペンディング状態、エラー処理、楽観的更新などをデータの送信を自動管理
- トランジションと統合されている
-
サーバーファンクションとアクションの違いは?
- サーバーファンクションがアクションから呼び出される場合は、サーバーアクション
- サーバーアクションに対応していないライブラリを使う場合でも、サーバーファンクションは使用可能
Actions (アクション)とは?
Actions は、フォーム送信やデータの変更(ミューテーション)を扱うための非同期遷移を使用する関数です。
React 19 で安定版として追加されました!
Actions は自動的にペンディング状態、エラー、フォーム、および楽観的更新を処理できます。
これにより、従来のイベントハンドラーと比較して、よりシンプルなコードで複雑な処理を実装できるようになりました!
Actions (アクション)の特徴
Actions の主な特徴は、以下の通りです:
- ペンディング状態の管理: リクエスト開始時にペンディング状態を自動的に設定し、最終的な状態更新が完了すると自動的にリセットします
-
楽観的更新:
useOptimistic
フックと連携して、リクエスト処理中にユーザーに即時フィードバックを表示できます - エラー処理: リクエストが失敗したときにエラー境界を表示し、楽観的更新を元の値に自動的に戻すことができます
Server Functions (サーバーファンクション)とは?
サーバーファンクション(Server Functions)は、クライアントコンポーネントがサーバー上で実行される非同期関数を呼び出せるようにする機能です。
上記の公式からの引用:
注: 2024 年 9 月までは、すべてのサーバー関数を「サーバーアクション」と呼んでいました。サーバー関数がアクションプロパティに渡されるか、アクション内から呼び出される場合はサーバーアクションですが、すべてのサーバー関数がサーバーアクションであるとは限りません。このドキュメントでは、サーバー関数が複数の用途に使用できることを反映するために、名称を更新しました。
さて、興味深いことに、2024 年 9 月まで、すべてのサーバーファンクションは「サーバーアクション」と呼ばれていました。
しかし、サーバーファンクションが必ずしもすべてアクションとして使われるわけではないため、
React 19 ではこの命名が変更されています!
ややこしいですね 😇
Server Functions (サーバーファンクション)の特徴
サーバーファンクションは、クライアントとサーバー間の通信を簡素化し、
安全にサーバーサイドの処理を実行するための強力な機能です。
"use server"
ディレクティブを使用して定義され、以下のような特徴があります:
- サーバーサイド専用の処理: データベースへのアクセスやファイルシステムの操作など、サーバー側でのみ実行したい処理を安全に行えます
- クライアントからの呼び出し: クライアントコンポーネントから直接呼び出すことができます
- 型安全性: TypeScript と連携することで、引数と戻り値の型チェックが可能です
- シリアライズ可能な値: 引数と戻り値はシリアライズ可能なデータ型である必要があります
Actions と Server Functions の違いは?
サーバーファンクションとアクションの関係は少し複雑です。
(というか筆者は、この部分で混乱したので記事を書くことにしました)
👀 要約図:
1. 関係性
サーバーファンクションが action
prop に渡されるか、action 内(transition
)で呼び出される場合、それはサーバーアクションとなります。
しかし、すべてのサーバーファンクションがサーバーアクションであるわけではありません。
つまり、サーバーアクションは、特定の文脈(主にフォームや特別なイベントハンドリング)で使用されるサーバーファンクションのサブセットと言えます。
(というか、単に古い言い方なのかも、です。)
2. 実行コンテキスト
React19 での更新を踏まえると、更新系のサーバーファンクションはトランジション内で呼び出されるべきです。
そして、<form action>
やuseActionState
に渡されるサーバーファンクションは、自動的にトランジション内で呼び出されます。
これにより、ユーザーインターフェースの応答性を維持しながら、バックグラウンドでサーバー処理を実行することができます。
Next.js における実装例
Next.js では、サーバーファンクションとアクションを簡単に実装できます。
以下では、それぞれの実装例を見ていきましょう。
サーバーファンクションの実装例
サーバーファンクションは、"use server"
ディレクティブを使用して以下のように定義できます:
- モジュールレベルでの定義:
// app/server.js
"use server";
// このファイル内のすべてのエクスポートされた関数はサーバーファンクションになります
export async function getUserData(userId) {
// サーバー側でのデータ取得処理
const user = await db.users.findUnique({ where: { id: userId } });
return user;
}
- 関数レベルでの定義(サーバーコンポーネント内):
// app/profile/page.jsx (サーバーコンポーネント)
export default function ProfilePage() {
async function getUserData(userId) {
"use server";
// サーバー側でのデータ取得処理
return await db.users.findUnique({ where: { id: userId } });
}
// サーバーコンポーネント内での使用
// または、クライアントコンポーネントにpropsとして渡す
}
サーバー関数を使用すると、クライアントコンポーネントはサーバー上で実行される非同期関数を呼び出すことができます。
なので、上記の関数は、クライアントコンポーネントで呼び出し、サーバー上で実行できます!
要は、上記のような、"use server"
付きのサーバーで実行される関数を、
クライアントで呼び出すということは、アクションでなくても可能です!
アクションの実装例
アクションは、非同期遷移を使用する関数です。
クライアントコンポーネントで呼び出すことができ、
フォームの送信やデータの変更をサーバー上で処理するために使用されます。
- フォーム(<form>の action プロップ)での利用例:
// app/contact/page.jsx
"use client";
import { useState } from "react";
import { submitForm } from "../actions";
export default function ContactForm() {
const [isPending, startTransition] = useTransition();
return (
<form action={submitForm}>
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<button type="submit" disabled={isPending}>
{isPending ? "送信中..." : "送信する"}
</button>
</form>
);
}
これは、import { submitForm } from "../actions";
が、
<form action={submitForm}>
に渡されるサーバーファンクションとして定義されています。
さらに、action
に渡されたことで、トランジション化されるので、サーバーアクションです!
useTransition
を使用できますね 👍
- useActionState を使用した例:
// app/signup/page.jsx
"use client";
import { useActionState } from "react";
import { createUser } from "../actions";
const initialState = { message: "" };
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">メールアドレス</label>
<input type="text" id="email" name="email" />
{state.message && <p className="error">{state.message}</p>}
<button type="submit" disabled={pending}>
{pending ? "処理中..." : "登録する"}
</button>
</form>
);
}
useActionState
を使用することで、サーバーアクションの状態管理を簡略化できます!
- startTransition でのカスタム呼び出し:
// app/dashboard/page.jsx
"use client";
import { useTransition } from "react";
import { deleteItem } from "../actions";
export function ItemList({ items }) {
const [isPending, startTransition] = useTransition();
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}
<button
onClick={() => startTransition(() => deleteItem(item.id))}
disabled={isPending}
>
{isPending ? "削除中..." : "削除"}
</button>
</li>
))}
</ul>
);
}
startTransition
を使用することで、手動でアクション化することも可能です!
もちろん、async/await を使用することでも可能です!
React 19 における注意点
React 19 では「サーバーアクション」から「サーバーファンクション」へと名称が変更されました。
これは、「use server
」関数の呼び出しの多くが、実際にはアクションではないためです。
例えば、以下のようなコードはサーバーファンクションですが、実際にはアクションとしては使われていません:
"use client";
import { createNote } from "./actions";
function EmptyNote() {
// これはクリック時にサーバー関数を呼び出しているだけで、
// 厳密にはアクションとは言えません
<button onClick={createNote} />;
}
なので、サーバーアクションは、アクションのコンテキストで使用されるサーバーファンクションと捉えるべきです。
React Hook Form などのライブラリとの連携
当記事執筆時点(2025/05/07)で、
React Hook Form など、まだサーバーアクションに対応していないライブラリがあります。
しかし、更新処理をサーバーで実行したいだけなら、サーバーファンクションで可能です!
(ややこしいですね 😇)
// app/form/page.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { saveUserData } from "../actions";
// バリデーションスキーマ
const userSchema = z.object({
name: z.string().min(2, "名前は2文字以上で入力してください"),
email: z.string().email("有効なメールアドレスを入力してください"),
});
type UserFormValues = z.infer<typeof userSchema>;
export default function UserForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<UserFormValues>({
resolver: zodResolver(userSchema),
});
const onSubmit = async (data: UserFormValues) => {
// フォームデータをFormDataに変換
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
formData.append(key, value);
});
// サーバー関数を実行
const result = await saveUserData(formData);
// 結果を処理
if (result.success) {
console.log("保存しました!");
} else {
console.error("エラーが発生しました:", result.error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">名前</label>
<input id="name" {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label htmlFor="email">メールアドレス</label>
<input id="email" type="email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "送信中..." : "送信する"}
</button>
</form>
);
}
この例では、
- React Hook Form で作成したフォームデータを、
FormData
に変換してからサーバーファンクションに渡しています - フォームの
action
プロパティにサーバーファンクションを直接渡していないため、トランジション化されていない - なので、厳密にはサーバーアクションではありません
- ただ、サーバー上で処理を実行できる点は同じです!
おわりに
最後まで読んでいただき、ありがとうございます 🥳
下記の、Next.js ハンズオン勉強会での、振り返りのような記事ですが、
少しでも参考になれば、嬉しいです!
そして、もし、間違いや補足情報などがありましたら、
ぜひコメントを追加してください!
Happy Hacking :)
参考
Discussion