Next.jsでResult型をServer/Clientを跨いで使うならErrorはクラスとして作らないほうがいいよ
結論
Result型を使うのはいいけど、それをServer -> Clientの処理で渡す場合、Resultのなかにクラスを持ち込まないようにしよう。
Server -> Clientの処理で渡すデータはPlainなオブジェクトである必要があるから。
対象読者
- ↑の結論を見てもよくわからない人
前提
- この文章ではResult型, Server Component, Server Functions(Server Actions) の基本的な概念については記載しません。
- この文章では「Result型そもそもいらなくない?」「tsのあらゆる実装においてclassは使うべきではない」という話については議論しません。
あらすじ (読まなくてもいいストーリー)
慣れないけどNext.jsを使ってフロントエンドを作り込むぜ!
モダンでイケイケなServer ComponentやらServer Functions(Server Actions)って機能があるんだよな!
Server Component内でのデータを取得したり、Server Functions内で更新することを考えると、その取得・更新処理のレスポンスはResult型で得られるとキレイかもしれない!
……できた!コンパイルエラーもないし動かしてみよう!
→ Only plain objects can be passed to Client Components from Server Components.
ServerからClientにデータを渡す
Next.jsにおいてServerからClientにデータが渡るのは以下の2つのシーンです。
- Server ComponentからClient Componentにpropsでデータを渡す。
- こっちは問題になりにくい
- Client ComponentからServer Functionsを使いデータを取得する
- Resultを導入するならこっちが怖い!
冒頭で述べた通り、この際にClient Componentが受け取るデータはPlainなオブジェクトである必要があります。
こういうの { name: "ポチ" }
メソッドをもたせたりするとサーバ側でのシリアライズ時にエラーになります。
Only plain objects can be passed to Client Components from Server Components.
こういうのはだめ { name: "ポチ", greet: () => "ポチですワン" }
同様にクラスのインスタンスを渡そうとする場合もエラーになったり、こっそりとプロパティが欠損したりします。
そもそも「Client ComponentからServer Functionsを使いデータを取得する」のは、ClientからfetchでAPIを叩いているようなものです。
そのため、冷静に考えてみればPlainなオブジェクトでないとマズイわけです。
そして、このエラーがまた厄介で、コンパイルエラーとしては発見されません!
実行時エラーとして出てきて初めて気づくことになります。
気をつけたいResult型
開発/設計をしていくうちにServer, Clientの境界を意識し忘れてしまったがために、コンパイルエラーを素通りして実行時に上記のエラーを見て落ち込むことはそこそこあるのではと思います。
小規模なミスだったら傷は浅いんですが、Result型のように実装の広範な箇所に跨るものでやらかすと手戻りで落ち込みます。
よくあるError型
Result型の前にErrorについて。
どの言語でも実装時にカスタムエラーを定義するとき、元々あるErrorクラスを利用することは多いと思います。ハンドリングするときに便利だし。
TSだとこんな感じかな。
export class CustomError extends Error {
code: number;
constructor(message: string, code: number = 500) {
super(message);
this.name = "CustomError";
this.code = code;
}
}
で、これをResult型のErrorの型に使うわけです。ここまではまだいいです。
ああ!でもそのResult型をClientに渡さないで!
トランスパイルされたコードの隙間から実行時エラーがあなたを見ている!
ということでServer Component内でデータフェッチをするときのレスポンスに、このResult型を使うのは問題ないんです。
でもServer Functionsのレスポンスに使ってClientに渡すのはいけません!
この使い分けはややこしいし間違えそうだなぁと僕は思います。
Server内用のResult型, Server/Clientを跨げるResult型と2つ用意するのもややこしいです。(でも、選択肢としてはあり)
でもResult型を使いたい
でもResult型を諦めきれません。僕は。
結論として、今回のようなシーンではこういうPlainなエラーオブジェクトを別途用意して、Result 型の中ではそれを使うのがいいんじゃないかなぁと思っています。
export type ServerError = {
name: string;
message: string;
code: number;
};
Result型を受ける側でエラーを真面目に捌いてユーザに表示するシーンではもっと工夫が要るような気もします。
おわりに
とっ散らかっていますがサンプルのリポジトリがあります。
捏ね回したい時に使ってみてください。
Discussion