Next.js の "use server" 制約と回避策

2026.02.02
Share:

はじめに

「なんでこれがエラーになるの?」

BandBridge のメッセージ機能を Claude Code で実装しているとき、理解できないエラーに遭遇しました。Server Actions を使ったファイルで定数をエクスポートしようとすると、ビルドが通らない。

コードを何度見直しても、間違いが見つからない。StackOverflow を検索しても、同じ問題に遭遇している人がいない。

結局、React のドキュメントを読み直して、ようやく理由が分かりました。

この記事では、"use server" ディレクティブの制約と、その回避策について共有します。同じエラーで時間を無駄にする人が減れば幸いです。

BandBridge とは

まず背景を説明させてください。

BandBridge は、ミュージシャン同士のマッチングサービスです。バンドを組みたい人が、メンバーを探したり、スカウトを受けたりできる。

このサービスでは、ユーザー間でメッセージのやり取りができます。スカウトを送る、質問する、条件を交渉する。そういったコミュニケーションのための機能です。

そして、メッセージ機能には「通報」機能も必要でした。

通報機能の実装

メッセージの中には、不適切なものが含まれる可能性があります。スパム、ハラスメント、詐欺的な勧誘など。

そういったメッセージをユーザーが通報できるようにする必要がありました。

通報機能の実装は単純です:

  1. ユーザーが「通報」ボタンを押す
  2. 通報のカテゴリ(spam、harassment など)を選択
  3. サーバーに通報データを送信
  4. 管理者がレビューして対応

Server Actions を使えば、フォームの送信からサーバー処理まで一貫して書けます。

問題発生:定数がエクスポートできない

通報のカテゴリを定数として定義しました。フォームのセレクトボックスでも、サーバー側のバリデーションでも、同じ定数を使いたかったからです。

// actions/report.ts
"use server";

// これがエラーの原因だった
export const REPORT_CATEGORIES = ['spam', 'harassment', 'inappropriate', 'other'] as const;

export type ReportCategory = typeof REPORT_CATEGORIES[number];

export async function submitReport(category: ReportCategory, messageId: string) {
  // 通報処理...
}

ビルドしてみると...エラー。

「Only async functions are allowed to be exported in a "use server" file.」

「え?定数をエクスポートしているだけなのに?」

最初は意味が分かりませんでした。

なぜダメなのか

2026年1月10日の開発日記にこう書きました:

"use server"の制約: Server Actionsのファイルから定数をエクスポートできない制約に遭遇。Reactのドキュメントを読み直して理解

React のドキュメントを読み直して、ようやく理由が分かりました。

"use server" ディレクティブは、そのファイルのエクスポートをすべて「サーバーで実行される関数」としてマークします。

これが何を意味するかというと...

Server Actions の仕組み

Server Actions は、クライアントから直接呼び出せるサーバー関数です。でも、実際にはサーバーで実行されます。

じゃあ、クライアントはどうやってサーバーの関数を呼び出すのか?

答えは「参照を渡す」です。

React は Server Actions を内部的に「このIDの関数を呼び出してください」という形式にシリアライズします。クライアントは、関数本体ではなく参照だけを持っている。

定数は参照できない

ここで問題が発生します。

定数やオブジェクトは、関数のように「参照」を作れません。値そのものをクライアントに送る必要がある。

でも、"use server" ファイルは「すべてのエクスポートがサーバー関数」という前提で動いています。定数やオブジェクトは、この仕組みに合わない。

だから、エラーになる。

解決策:ファイルを分ける

開発日記に解決策を記録しました:

report-types.ts分離: "use server"ディレクティブのあるファイルから定数をエクスポートするとエラーになる。型/定数を別ファイルに分離

シンプルな解決策です。定数と型を別ファイルに切り出す。

src/
├── actions/
│   └── report.ts      # Server Actions(関数のみ)
└── types/
    └── report-types.ts # 定数・型

types/report-types.ts の内容:

// types/report-types.ts
// "use server" なし!

export const REPORT_CATEGORIES = ['spam', 'harassment', 'inappropriate', 'other'] as const;

export type ReportCategory = typeof REPORT_CATEGORIES[number];

actions/report.ts の内容:

// actions/report.ts
"use server";

import { ReportCategory } from '@/types/report-types';

export async function submitReport(category: ReportCategory, messageId: string) {
  // 通報処理...
}

これで、クライアントコンポーネントからは両方をインポートできます:

// components/ReportButton.tsx
"use client";

import { REPORT_CATEGORIES, ReportCategory } from '@/types/report-types';
import { submitReport } from '@/actions/report';

function ReportButton({ messageId }: { messageId: string }) {
  // REPORT_CATEGORIES を使ってセレクトボックスを作る
  // submitReport を使って送信する
}

ファイル構成のパターン化

この問題に遭遇してから、Server Actions を使うプロジェクトではファイル構成を統一するようにしました。

src/
├── actions/           # Server Actions(関数のみ)
│   ├── report.ts
│   ├── user.ts
│   ├── message.ts
│   └── band.ts
└── types/             # 型・定数・enum
    ├── report-types.ts
    ├── user-types.ts
    ├── message-types.ts
    └── band-types.ts

「Server Actions のファイルには関数しか置かない」というルールを決めておけば、同じエラーに二度と遭遇しません。

最初からこの構成にしておけばよかった、と思いました。でも、エラーに遭遇して初めて気づくことって、よくありますよね。

エラーハンドリングも追加した

同じ日の開発では、Claude Code と一緒にエラーハンドリングの問題にも対処しました。

開発日記から:

error.tsx / not-found.tsx: エラー時のUIがなかったため、ユーザーに何が起きたか伝えられなかった

Server Actions がエラーを投げたとき、画面が真っ白になることがありました。何も表示されない。

ユーザーからすると「何が起きたか分からない」状態。これは最悪の UX です。

error.tsx:エラーバウンダリ

Next.js App Router では、error.tsx を配置すると自動的に Error Boundary になります。

同じディレクトリ以下で発生したエラーをキャッチして、代替の UI を表示できます。

開発日記の悩みポイント:

エラーバウンダリの設計: どこまでの情報を表示するか。開発環境ではスタックトレース、本番では汎用メッセージ

本番環境でスタックトレースを見せるのは、セキュリティ上よくありません。内部の実装が漏れてしまう。

でも、開発環境では詳細なエラー情報があった方がデバッグしやすい。

NODE_ENV で切り替えることにしました:

  • 開発環境: エラーメッセージとスタックトレースを表示
  • 本番環境: 「エラーが発生しました。再試行してください」のような汎用メッセージ

not-found.tsx:404 ページ

存在しないメッセージにアクセスしたとき用の 404 ページも追加しました。

開発日記の学び:

not-found.tsxはnotFound()関数と組み合わせて使う

Server Component の中で notFound() を呼ぶと、同じディレクトリの not-found.tsx がレンダリングされます。

たとえば、メッセージの詳細ページで:

// app/messages/[id]/page.tsx
import { notFound } from 'next/navigation';

async function MessagePage({ params }: { params: { id: string } }) {
  const message = await getMessage(params.id);

  if (!message) {
    notFound(); // これで not-found.tsx が表示される
  }

  return <MessageDetail message={message} />;
}

ルートごとにカスタマイズできるのが便利です。メッセージ用の 404、バンド用の 404、ユーザー用の 404 など、それぞれ違うメッセージを出せます。

この経験から学んだこと

1. フレームワークの仕組みを理解する

"use server" がなぜ定数をエクスポートできないのか。最初は「バグじゃないの?」と思いました。

でも、React のドキュメントを読んで仕組みを理解すると、「なるほど、そういう設計か」と納得できました。

フレームワークの表面的な使い方だけでなく、内部の仕組みを理解することが大切です。そうすれば、エラーに遭遇したときも「なぜダメなのか」が分かる。

2. 最初からファイル構成を決めておく

後からリファクタリングするより、最初からルールを決めておく方が楽です。

「Server Actions のファイルには関数のみ」 「定数と型は types/ ディレクトリ」

このルールを最初から決めておけば、同じエラーに遭遇することはなかったはずです。

3. エラーハンドリングは後回しにしない

「まずは動くものを作って、エラーハンドリングは後で」と思いがちです。

でも、エラーハンドリングがないと、問題が起きたときにデバッグが難しくなります。何が起きているか分からない。

最低限の error.tsxnot-found.tsx は、最初から入れておくことをお勧めします。

まとめ

問題解決策学び
定数をエクスポートできないtypes/ ディレクトリに分離Server Actions の仕組みを理解する
エラーが見えないerror.tsx を配置エラーハンドリングは後回しにしない
404 の表示がないnot-found.tsx を配置ルートごとにカスタマイズ可能

Server Actions は便利ですが、"use server" の制約を理解しておく必要があります。

「なぜエラーになるのか」を調べる過程で、React の Server Components の仕組みについて理解が深まりました。エラーは学びのチャンスでもあります。

この記事が、同じエラーで悩む人の参考になれば幸いです。


関連記事

Zeronova avatar

Zeronovaゼロノバ

Product Manager / AI-Native Builder

Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。

関連プロダクト

BandBridge(バンドブリッジ)

ミュージシャンとバンドをつなぐ