はじめに
「なんでこれがエラーになるの?」
BandBridge のメッセージ機能を Claude Code で実装しているとき、理解できないエラーに遭遇しました。Server Actions を使ったファイルで定数をエクスポートしようとすると、ビルドが通らない。
コードを何度見直しても、間違いが見つからない。StackOverflow を検索しても、同じ問題に遭遇している人がいない。
結局、React のドキュメントを読み直して、ようやく理由が分かりました。
この記事では、"use server" ディレクティブの制約と、その回避策について共有します。同じエラーで時間を無駄にする人が減れば幸いです。
BandBridge とは
まず背景を説明させてください。
BandBridge は、ミュージシャン同士のマッチングサービスです。バンドを組みたい人が、メンバーを探したり、スカウトを受けたりできる。
このサービスでは、ユーザー間でメッセージのやり取りができます。スカウトを送る、質問する、条件を交渉する。そういったコミュニケーションのための機能です。
そして、メッセージ機能には「通報」機能も必要でした。
通報機能の実装
メッセージの中には、不適切なものが含まれる可能性があります。スパム、ハラスメント、詐欺的な勧誘など。
そういったメッセージをユーザーが通報できるようにする必要がありました。
通報機能の実装は単純です:
- ユーザーが「通報」ボタンを押す
- 通報のカテゴリ(spam、harassment など)を選択
- サーバーに通報データを送信
- 管理者がレビューして対応
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.tsx と not-found.tsx は、最初から入れておくことをお勧めします。
まとめ
| 問題 | 解決策 | 学び |
|---|---|---|
| 定数をエクスポートできない | types/ ディレクトリに分離 | Server Actions の仕組みを理解する |
| エラーが見えない | error.tsx を配置 | エラーハンドリングは後回しにしない |
| 404 の表示がない | not-found.tsx を配置 | ルートごとにカスタマイズ可能 |
Server Actions は便利ですが、"use server" の制約を理解しておく必要があります。
「なぜエラーになるのか」を調べる過程で、React の Server Components の仕組みについて理解が深まりました。エラーは学びのチャンスでもあります。
この記事が、同じエラーで悩む人の参考になれば幸いです。
関連記事
Zeronova(ゼロノバ)
Product Manager / AI-Native Builder
Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。