これはなんの記事か?
honoはJSXを処理する実装をもっています。JSXはJavaScriptの構文を拡張したもので...のような話は皆さんご存知でしょうしググってもらった方が正確な情報を得られると思うのでここでは書かずに、honoの実装での特徴的なところについて書いていこうと思います。
特徴は?
文字列の出力に特化していることです。Reactでいうところの`renderToString(<App />)` のみに対応しています。`<App />.toString()` が文字列になります。`document.body.innerHTML = <App />`が動くのはそのためです。
esm. sh/runのやつ、HTMLに内で書いたJSXコンポーネントをそのままHTMLとして渡すなんてことができる。意味不明でよい pic.twitter.com/TD7AwSQ7HI
— Yusuke Wada (@yusukebe) November 27, 2023
Reactとの互換性は?
一般的なHTMLの要素をはじめ、Function Component、Fragment、dangerouslySetInnerHTMLなど、JSXの基本的な構成要素は揃っているので、特に戸惑うこと無くJSXを書いてHTMLを出力することができると思います。
非互換の一番大きな点としては、今まで書いたことはなく質問されたこともなかったのですが、`Component`のクラスに対応していないことかもしれません。とはいえ、文字列の出力に特化していて状態を持ちたいことはないと思うので、クラスが必要になる場面は基本的にはないと思います。
地味な違いとしては"class"を"className"と書く必要がないので、HTMLをそのまま持ってきて必要なところだけコンポーネントに置き換えるときに楽です。そこはpreactと一緒ですね。(ただ逆にReactから持ってくるときにはclassNameをclassに置き換える必要がありますが。)
ベンチマークは?
honoといえば「Ultrafast & Lightweight」なのでその点にもこだわっています。
honojs/hono/tree/main/benchmarks/jsx にベンチマークがあり、React、Preact、Nano、という本家+メジャーなJSX処理系と比較しています。「honoに実装されている機能の範囲で比較した場合」というベンチマークなのでhonoに有利な条件ですが、この中で最速になっています。
またベンチマークの例でバンドルした場合のサイズ(honoの本体は含まずに、JSXの処理系だけをバンドルしたサイズ)は3.9kbと小さく、これも比較した中で最小です。
JSXの処理系の中で最速でしょうか?
ルーターのベンチマークに関してはRegExpRouterが「必要十分な機能を持ったルーターとしてはJSの中で最速」となっていますが、JSXに関しては少し状況が違います。
honoのJSXでは(他の多くのJSXの処理系と同じように)一度データ構造の木を作成して、そこから文字列化を行っています。一度データ構造を作ることで親コンポーネントが子を管理しやすくなり、細かい要望に応えやすくなります。(後述するErrorBoundaryで同期的エラーを補足する場合とか)
ただJSXの文字列化のアプローチとしてはこの「データ構造の木を作成」というところを省略する方法もあり、その場合には細かい管理はできないもののより高速に処理できます。このアプローチを採用している処理系には@kitajs/htmlがあり、これは速度だけをみるとhonoのJSXよりも高速です。
非同期なコンポーネント
honoの3.10からは非同期なコンポーネントを使うことができるようになりました。
`<App />.toString()` がPromiseを返すようになってしまう気持ち悪さはありますが、受け取る側でラップすれば通常の文字列と同じ感覚で使うことができます。
これは実はReactとも互換性があって、18.2まではエラーになるのですが、Reactの18.3(2023年12月現在は@nextのステータス)では`renderToReadableStream`を使うことで文字列にすることができます。Suspenseも不要です。
完全な互換性があるわけではないのであまり不用意なことは言えないのですが、honoのJSXは独自の実装ではあるものの意外と本家の挙動も意識しており、後述するSuspenseにおいても相互の乗り換えで混乱することがないようにしています。
もちろんですが、返されたPromiseは並列で処理されるので、同時にたくさんの非同期なコンポーネントを使っても大丈夫です。
Suspense
honoの3.10で非同期なコンポーネントのサポートすると同時に、Reactで使えるSuspenseも用意しました。
前項の話とも重なりますが、Reactの18.2まではSuspence[fallback]を使う場合にはPromiseをthrowするというのが作法だったと思いますが、18.3ではPromiseをreturnでもいけるようです。(Reactには詳しくないのですが。)honoでは、従来どおりのthrowする方法も、returnする方法も、どちらもサポートしています。(関連する機能としては`use`というhookもあり、honoでも一度実装したのですが、honoでは単に async/await で十分であり必要な場面がないだろうということで削除しました。)
以下のコードはhonoでもReact(18.3)でもどちらでも、最初に「Loading...」が表示され、fetchの完了後に内容が更新されるという動作になります。
ErrorBoundary
ErrorBoundaryはReactの本体では提供されていないのですが、bvaughn/react-error-boundaryという公式のドキュメントからもリンクされていて広く使われているライブラリがあり便利そうだったので、仕様を真似て実装しました。
以下のコードで、最初に「Loading...」が表示され、その後例外が投げられたタイミングで「Something went wrong」に更新されるという動作になります。
ErrorBoundary[onError]やErrorBoundary[fallbackRender]を使って、ログを記録したり、エラーの内容に応じて出力を切り替えることもできます。
以上、honoのJSXの実装の紹介でした。
これは Hono Advent Calendar 2023 の13日目の記事です。引き続きよろしくお願いします!