giraphme/blog

Server Actions との向き合い方を考える

先日 Next.js 14 がリリースされ、ついに Server Actions が Stable になりました。

Twitter で観測している限りでは賛否両論のようですが、良く切れる包丁と同じく便利な機能との向き合い方は常に整理しなければなりません。

まだ本番環境で扱う機会を得られていないので、小さなサンプルプロジェクトですが、いくつかのパターンで扱ってみたので要点を整理してみます。

##TL;DR

  • Server Actions では JSX.Element を返すことも可能だが、普通の REST API を設計する場合と同じく Plain Object を返す方が無難

  • UI とロジックの責務分割の観点から page.tsxactions.ts は分けた方が管理しやすい

  • 型付き fetcher の生成関数として扱う

##Server Actions とは何か

サーバーサイドで実行する必要のある処理を関数として定義し、クライアントサイドでシームレスに使用できるようになる仕組みのようです。

Next.js のドキュメントには

Next.js integrates with React Actions to provide a built-in solution for server mutations.

とありますが、 React Actions については React のドキュメントにて記述を見つけることができませんでした。

"use server" のディレクティブのことではないか?と邪推していますが詳細がわかったら追記します。

さて、その Server Actions は以下のようなサンプルコードとともに Nextjs Conf 2023 で紹介されました。

また、コンセプト的なサンプルとして以下のようなコードも紹介されています。

どちらも API Routes などによって API 実装を行わず、コンポーネント側から直接サーバーの動作を指定しているのが印象的です。

特に Bookmark コンポーネントについては SQL を直接実行していることから、面白いなと思う反面、否定的な意見を持たれた方も少なくないのではないでしょうか。

私自身も、どちらのサンプルも本番運用を想定していないサンプルコードだと受け取っていますが、本番運用を想定する場合はどのように Server Actions と向き合うと良いのでしょうか。

##実際に使ってみる

では早速、小さなプロジェクトで試していきます。今回試したコードはここに置いています。

ライブラリについては、少しだけ本番運用を意識して下記を入れています。

  • react-hook-form

  • zod

  • neverthrow

  • Panda CSS

  • react-toastify

なお、ログイン画面を想定した UI を使用し成功・失敗を toast で表示する仕様としています。(ログイン後のリダイレクトなどの詳細な実装はやっていない)

※ Next.js Conf 2023 のサンプルなどでは page.tsx に Server Actions が定義されていますが、 UI とロジックの責務を分割するためすべてのサンプルで actions.tsx? に分離しています。

###JSX を返すサンプル

Server Actions では Plain Object やビルトインされたいくつかの Object などを返すことができます。

なお、 async は必須です。

このサンプルではひとまず成功の JSX を返すだけにしています。

下記のように react-toastify に渡してあげます。

たったこれだけで添付のようにスタイル付きの toast を表示することができました。

ログインに成功したトーストが表示されている

JSX.Element は Server Actions にビルトインされている Object であり、Server Actions から以下のようなレスポンスが返ることで自動的に JSX.Element に変換されます。

###エラーハンドリング

先ほどの Server Action は成功時のメッセージのみ実装しているので、ログイン失敗時のサンプルも見てみましょう。

対する page.tsx は下記です。(先述のサンプルとの差分は submitWithFeedback のみなので、抜粋して記載しています。)

ログインの成功・失敗を判断するという意味ではこれで動作するようになりました。

しかし、JSX.Element を返して Error を throw する実装だと、失敗する理由が複数ある場合や、「ログインは成功したがパスワード更新など追加のアクションが必要」など成功にバリエーションがある時などにフロントエンドで適切にフィードバックすることが難しくなります。

実際のログイン画面だとログイン成功時にはリダイレクトをしたり追加の Server Actions の呼び出したりする場合もあるので、より複雑化することは想像に難くないでしょう。

また、 Error を throw した場合、 Server Actions は HTTP Status 500 を返すため、 mutation の成否の判断としてはあまり標準的な動作ではなく、選定しているエラー監視ライブラリによってはノイズになることも想定されます。

Error を throw した図

以上により、Server Actions で Mutation を扱う場合には、 JSX.Element を返して Error を throw する仕様は避けるべき実装ではないかと考えています。

###neverthrow を使う

さて、 Object を返してエラーを throw しないとなると、私は Result 型を使うことが多く、その便利ライブラリとして neverthrow を選定することが多々あります。

Server Actions は async にする必要があるため、ResultAync ではなく Promise<Result<string, string>> としています。

そして pages.tsx の submitWithFeedback は以下のようにしました。

JSX を返すサンプルの項でも書きましたが、Server Actions では Plain Object やビルトインされたいくつかの Object などを返すことができます。

そのため、 result.isOk() などの関数を含む neverthrow の Object を Server Actions で扱うことができず、エラーになってしまいました。

Result 型を独自定義せずにライブラリで運用したい場合、 Plain Object で扱えるものを選定する配慮が必要そうです。

###オレオレ Result 型を返す

Plain Object しか扱えないので、オレオレ Result 型を作って似たようなレスポンスを定義してみます。

だいぶ雑ですが、型安全に Server Actions を扱うことができるようになりました。

Success<T> を調整すれば、複雑なフロントエンドでのフィードバックも簡単に実装可能になります。

##所感

後半になるほど解説が雑になりましたが、いくつかのサンプルを見てきました。

Server Actions はコーディング時のインターフェイスはただの関数ですが、実行時には fetch を呼び出しているだけなので、型付き fetcher の生成関数と捉えると少し扱いやすくなるのではないでしょうか。

また従来の API Routes では、コンポーネントとの距離が遠くロジックが分散していましたが、 actions.ts に定義することによって影響範囲がわかりやすくなり、不要となった関数の整理もやりやすくなるように感じています。

一方で、 page.tsx にも定義可能という性質上、ロジックと UI の責務分割について意識しなければあっという間に破綻しやすいプログラムができあがってしまう可能性もあるため注意が必要です。

API Routes で API を実装し、fetch のレスポンスに個別に型をつけるという従来のコーディングスタイルと比べると、Server Actions は圧倒的に便利な機能だと考えているので、プロジェクトの方針に合わせて取り込んで行きます。

© giraph.me