これは何か?

カスタムブロックは非常に拡張性が高いのですが、「入力フォーム兼テンプレートとなるHTMLはブロックエディタで用意する」「挿入するカスタムスクリプトはHTML形式で、style要素やscript要素を混ぜたものになる」というところで、ローカル環境で開発がやややりにくいところがあったので、その問題を解決するべく開発ツールを作ってみました。

以前のものとの違い

カスタムブロックの開発ツールの用意は以前も試みたことがあり、そのときにはVSCodeの拡張機能として「MT Custom Block Editor」というものを作っていました。(記事はここです)これはVSCodeとの連携の試みとしては面白かったのですが、VSCodeに強く結びついてしまうため他のcliツールとの連携が逆に難しくなる面がありました。

そのような折、去年から今年にかけていくつか出したnpmをベースにしたツールが意外と興味をもってもらえたのもあり、こちらのアプローチに変えてみようと思い、以前のVSCode版とは違うものとして作りました。

こんな感じで動きます。

npm create @usualoma/mt-custom-block

このコマンドで雛形を作成できて、動画のサンプルのカスタムブロックも最初から入っています。

.jsonのファイル自体は、ゼロからつくるよりも一回MTで簡単な設定をしてから書き出すほうが楽だと思います。そこをベースにして、このツールでは主に以下のデータを更新できます。

  • アイコン
  • JavaScript または TypeScript
    • .tsというファイル名で保存すると勝手にトランスパイルします
    • script[type=module]で挿入されるので、DOMContentLoadedを明示的に書く必要はありません
  • 入力フォーム兼テンプレートとなるHTML

上の2つはエディタから、一番下はブラウザから更新します。(ブラウザの保存ボタンでちゃんとファイルも更新されます)

デモ動画をみてもらうとなんとなく分かりますが、エディタでTypeScriptを変更して保存するとブラウザの方も自動でリロードされ、TypeScript(カスタムスクリプト)の実行結果をすぐに確認できます。カスタムブロックの作成で一番手間がかかるのがここだと思うので、ここですぐに反映されるのは嬉しいのではないでしょうか。

データとしては本来、カスタムスクリプトにはlink要素やstyle要素を書くこともできるのですが、そのあたりはJavaScriptで動的に対応できるので、このツールでは割り切ってJavaScriptのみで動かすようにしています。

注意事項

ビルドのプロセスを経ることで、意図せずにJavaScriptを大きくなってしまいがちである点には注意が必要です。大きくなるとCMSのシステム側への負荷も大きくなるのでご注意ください。script[type=module]で読み込まれるので、ブラウザ上でのimportも普通に使えるのでnpmのモジュールの読み込みはcdnからの直接の読み込みなどを利用するのがおすすめです。

この記事について

この記事はMovable Type Advent Calendar 2024の25日目の記事です!

今年はMTDDC Meetup TOKYO 2024にも参加ささせていただいて直接いろいろな人と話ができ、そしてこのアドベントカレンダーでいつも通りの年末感も味わうことができました。どちらも主催者であった西山さんをはじめ、関係者のみなさん、ありがとうございました。おつかれさました。

それではみなさん良いお年を!

これは何か?

Movable Typeの管理画面のfaviconを自分の好きな画像に差し替えるプラグインです。画像はウェブブラウザに保存されます。

mt-plugin-ShortcutIcon

対応ウェブブラウザ

  • Google Chrome : 任意の画像を設定できます
  • Firefox : SVGやPNGは設定できますが、JPEGは設定できません
  • Safari : 設定されません

開発の動機

@usualoma/mt-plugin-builderを使って何ができるかをもう少し考えたくて、「ウェブブラウザだけでできること」というテーマで作りました。

仕組み

ユーザーが指定した画像をIndexedDBに保存して、ページ表示の切り替わりごとに読み出してURL.createObjectURLを使ってURLを生成して、link[rel="shortcut icon"] を作成してheadに挿入するという動作をしています。

誰のためのものか?

faviconを設定するだけなら画像のリソースもプラグインに含んでしまえばよく、そうすればユーザー毎に設定せずともインストールしたMT全体で差し替えることができます。

しかしここで、「制作会社のユーザー」と「クライアントのユーザー」で考えると、クライアントのユーザーは複数のサイトのMTを使うわけではないのでMTのデフォルトでよく(むしろそれが自然)、制作会社のユーザーは複数のサイトのMTを使うのでfaviconで「どのサイトのMTか?」を区別できるとよかったりすると思います。そのようなわけで意外と「ユーザーが自分の好きなfaviconを設定できる」という機能にも需要があるのではないかと思っています。

この記事について

この記事はMovable Type Advent Calendar 2024の12日目の記事です!

HonoのJSXでPolyglotなSVGからQuineでイルミネーション

  • 投稿日:
  • by
  • カテゴリ:

こんにちは。OSS珍百景として紹介されたPRの作者の天野です。

今年は以下のあたりを中心にHonoにコミットしました

  • v3.12.0
    • CSRF Protection Middleware
    • css Helper
  • v4.0.0
    • Client Components
  • v4.5.0
    • Combine Middleware
    • React 19 Compatibility

この記事ではこれらの機能の実現に使った内部の機能に触れつつ、PolyglotなSVGを使ってHonoのJSXの表現力を試してみたいと思います。

できたもの

hono-polyglot-svgができました。以下のSVG画像ファイルはそのまま出力するとオリジナルのHonoのアイコン画像が表示されますが、HonoのJSXを経由すると、色を変えつつ自分自身を出力するQuineなSVG画像として出力されます。

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="500px" height="500px" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.993" fill="#ff5b11" d="M 257.5,0.5 C 258.822,0.330034 259.989,0.663368 261,1.5C 298.193,46.8936 333.193,93.8936 366,142.5C 390.289,179.069 410.955,217.735 428,258.5C 455.221,331.104 441.054,394.271 385.5,448C 336.892,489.082 280.892,505.082 217.5,496C 141.727,480.551 90.2265,436.718 63,364.5C 55.9081,340.989 53.5748,316.989 56,292.5C 60.0384,250.347 70.0384,209.68 86,170.5C 92.6509,154.514 101.318,139.848 112,126.5C 120.715,136.88 129.048,147.547 137,158.5C 140.682,162.349 144.515,166.016 148.5,169.5C 178.917,109.136 215.251,52.8028 257.5,0.5 Z"/></g>
<g><path style="opacity:1" fill="#ff9758" d="M 250.5,81.5 C 287.193,124.06 320.36,169.393 350,217.5C 359.293,233.418 366.959,250.085 373,267.5C 385.584,317.008 372.084,357.842 332.5,390C 294.216,416.939 252.216,424.939 206.5,414C 157.201,398.702 128.701,365.535 121,314.5C 119.131,298.409 120.798,282.742 126,267.5C 133.418,248.663 142.418,230.663 153,213.5C 163,198.833 173,184.167 183,169.5C 205.716,140.29 228.216,110.957 250.5,81.5 Z"/></g>
<happy-holidays-script>{
(async () => {
const replaceColors = () =>
Object.assign(
new String(
`{( async () => { const replaceColors = ${replaceColors}; return replaceColors(); })()}`
),
{
isEscaped: true,
callbacks: [
({ buffer }: { buffer: string[] }): undefined => {
const colorMap = {
"ff5b11": "1167ff",
"ff9758": "5897ff",
};
Object.entries(colorMap).forEach(([from, to]) => {
colorMap[to] = from;
});
if (buffer) {
buffer[0] = buffer[0]
.replace(new RegExp(`(?:${Object.keys(colorMap).join("|")})`, "g"), (match) =>
colorMap[match]
);
}
},
],
}
);
return replaceColors();
})()
}</happy-holidays-script>
</svg>
view raw hono.svg hosted with ❤ by GitHub
  • http://localhost:3000/static/hono.svg : そのまま出力
  • http://localhost:3000/static/hono.svg?eval=1 : JSXで処理して出力

です。以下のような結果になります。

解説

コールバックから書き換える

HonoのJSXの文字列化では、要素の属性値や内容としてPromise<string | String>を返すことができ、useを使うことなしにasyncな関数コンポーネントから簡単に文字列の結果を出力することができます。さらにここでStringに対してisEscapedcallbacksを指定することで、出力される際の挙動をコントロールすることができます。

この特殊なStringを作成するためには、通常は"hono/utils/html"からエクスポートされているraw()を使いますが、使わずに作成することもできます(内部の構造は今後変更される可能性もあるので、推奨はされませんが)。

import { raw } from "hono/utils/html"
raw("Hono &lt;(_ _)&gt;"); // "Hono <(_ _)>"
Object.assign(new String("Hono &lt;(_ _)&gt;"), {
isEscaped: true, // すでにエスケープ済みの場合には true にする
callbacks: [() => {}], // 文字列化のタイミングで呼ばれる
})
Object.assign(new String("Hono <(_ _)>"), {
isEscaped: false, // false にすると出力時にエスケープされる
})

callbacksに指定した関数が呼び出されるタイミングは、単純な文字列化以外にもrenderToReadableStream()を使ってレスポンスを返す前後のタイミングなど、いくつかあるのですが、ここでは深入りせず単純な文字列化を前提とします。以下のようにしてcallbacksに指定した関数で渡されたbufferの値を変更すると、出力結果を書き換えることができます。

<strong>Hono is fast</strong>
<p>{Promise.resolve(Object.assign(new String("Hono is lightweight"), {
callbacks: [({ buffer }) => {
buffer[0] = buffer[0].replace(/(fast|lightweight)/g, "really $1");
}],
}))}</p>

これが今回の、処理するたびにSVG画像の色が変わる挙動の仕組みです。Honoに実装された機能の中では「css Helper」でのstyle要素の埋め込みや、「React 19 Compatibility」でのtitle要素のhoistingで使われています。

(無駄に)Combine Middlewareを使う

「同じパスのURL」でありながら「処理方法(クエリストリングで指定)によって結果が変わる」ということを表現したかったので、Combine Middlewareのsome()を無駄に使って宣言的に書いてみました。以下のように書くと「evalが指定されたときにのみevalSvgMiddlewareを適用する」という挙動になります。

import { some } from "hono/combine";
const app = new Hono();
app.get(
"/static/*",
some((c: Context) => !c.req.query("eval"), evalSvgMiddleware),
serveStatic({ root: "./" })
);
view raw app.ts hosted with ❤ by GitHub

今回の例だと無理矢理感がありますが、hono.devのドキュメントでは認証と組み合わせた実用的な例も紹介されています。

解説は以上になります。

今年の振り返り

さて今年といえばHonoにとってはHono Conference 2024 - Our first stepの開催が一番のイベントだったと思います。(参加レポート書いてなかったですが。)コミッターの方やコメントをしてくれる方の顔を見られて、どんな話し方をするのかを知ることができたのは嬉しかったです。議論が円滑に進むかどうかはまた別の話だとしても、「GitHubのコメントが当人の声で再生できるようになる」というのは、やはりよいものだと思いました。

この記事について

これは Hono Advent Calendar 2024 のシリーズ 1の6日目の記事です。シリーズ 2もあるようなので、引き続きよろしくお願いします!

prettierで整形できて、minifyして書き出せる、MTML

  • 投稿日:
  • by
  • カテゴリ:

こんにちは、天野と申します。Movable Typeの開発に参加しています。

今日はMTML(Movable Type Markup Language)の話です。

MTMLを気持ちよく編集したい

公開される記事の典型的なMTMLは以下のようなものになります。

<!DOCTYPE html>
<html lang="<mt:BlogLanguage>">
<head>
<meta charset="<mt:PublishCharset>">
<title><mt:EntryTitle encode_html="1"> | <mt:BlogName encode_html="1"></titl>
<meta property="og:type" content="article">
<meta property="og:title" content="<mt:EntryTitle> | <mt:BlogName encde_html="1">">
<meta property="og:image" content="<mt:EntryAssets type="image" limit="1" file_ext="jpg,jpeg,png,gif"><mt:AssetThumbnailURL width="1200"><mt:Else><mt:BlogURL encode_html="1">images/ogimage.jpg</mt:EntryAssets>">
</head>
<body>
<mt:EntryBody>
<mt:EntryMore>
</body>
</html>
view raw entry.mtml hosted with ❤ by GitHub

HTMLの属性の中にMTタグが入っていますが、「カスタム要素の入ったHTML」と思ってみれば特別見にくいものでもないと思います。VSCodeにMTMLの拡張を入れれば、属性の中にも色がついて見やすくなります。

ただMTMLはやはり独自のマークアップ言語で一般的なツールではパースできないので「自動整形ができない」「対応するタグで閉じられているか編集時に検証できない」という問題があります。上のGistの内容だと「</titl>」となっていてHTMLが壊れていますし、「encde_html」となっている箇所もあります。また「<mt:EntryTitle>」というのは「encode_html」を付け忘れていると思われます。

とういところで、「JSXで書ければ整形も検証も一般的なツールでできるのになぁ」というのを思ったのです。

MTMLとJSXの関係については以前もmt-data-api-react としてPoC的なことをやったことがあります。これは「既存のMTMLをそのままで、DataAPIから取得したデータでフロントエンドでレンダリングする」というテーマでした。今回はそれとは異なり「JSXを書いて、それをMTが処理できるMTMLに変換する」というものです。

JSXに馴染みのない方はここまで読んでnot for meだなと感じるかもしれませんが、この話はそうでもないのでもう少し読み進めてください。以下は前出のMTMLを書き換えたものですが、ほぼほぼXMLとして違和感はないだろうと思います。(ないということで話を進めます。)またこれはJSXとしてもValidであり、これを処理することができます。

<html lang={<mt:BlogLanguage />}>
<head>
<meta charset={<mt:PublishCharset />} />
<title>
<mt:EntryTitle /> | <mt:BlogName />
</title>
<meta property="og:type" content="article" />
<meta
property="og:title"
content={
<>
<mt:EntryTitle /> | <mt:BlogName />
</>
}
/>
<meta
property="og:image"
content={
<mt:EntryAssets
type="image"
limit={1}
file_ext={["jpg", "jpeg", "png", "gif"]}
>
<mt:AssetThumbnailURL width={1200} />
<mt:Else />
<mt:BlogURL />
images/ogimage.jpg
</mt:EntryAssets>
}
/>
</head>
<body>
<mt:EntryBody />
<mt:EntryMore />
</body>
</html>
view raw entry.mtml.tsx hosted with ❤ by GitHub

Reactを書いている方は「JSXとしてもValid」というところで「Validかもしれないけど(TSXでは)型のエラーが出るよね」という話になってくると思いますが、JSXをReactでないJSXの処理系で型を付ければこれもいけます。

説明が長くなってきたので結果を見てみると、今回作ったmt-jsx-templateというコマンドで処理すると以下のような結果になります。

<html lang="<mt:BlogLanguage encode_html="1">"><head><meta charset="<mt:PublishCharset encode_html="1">"/><title><mt:EntryTitle encode_html="1"> | <mt:BlogName encode_html="1"></title><meta property="og:type" content="article"/><meta property="og:title" content="<mt:EntryTitle encode_html="1"> | <mt:BlogName encode_html="1">"/><meta property="og:image" content="<mt:EntryAssets type="image" limit="1" file_ext="jpg,jpeg,png,gif"><mt:AssetThumbnailURL width="1200" encode_html="1"><mt:Else><mt:BlogURL encode_html="1">images/ogimage.jpg</mt:EntryAssets>"/></head><body><mt:EntryBody><mt:EntryMore></body></html>

JSXで整形やバリデーションができる上に、つまるところJSXからMTMLへのトランスパイルになるので、その過程でMTMLをminifyして書き出すことができるのです。

実装部分は、honoというOSSのプロジェクトでJSXの処理系を実装したことがあったので、そこから一部をコピーしつつひとまず形にしました。

ちなみにTypeScriptで型も付けられるので、「encoee_html」もちゃんとエラーにできます。またトランスパイルのついでに「<$mt:EntryTitle$>」に自動で(デフォルトで)encode_htmlをつけることもできます。これは、結構いいんじゃないでしょうか。

JSXになればできること

JSXになると、自分でパースの努力をしなくてよくなるので「使われているCSSのクラス名を収集する」ということもやりやすくなります。(今までのテンプレートでも正規表現で収集すれば実用上十分な精度でできたと思いますが。)「JSXで書いてトランスパイルする」という処理が作業フローに組み込まれるのであれば、そのついでに「CSSのフレームワークから必要なCSSを抽出して最小限のCSSを作成する」というのもやろうかという話になってくると思います。

以下が、MasterCSSを使ったサイトのJSXテンプレートからクラス名を抽出して、使われているCSSのみを出力するイメージです。

<html lang={<mt:BlogLanguage />}>
<head></head>
<body>
<h1 class="font-size:24px font-weight:bold color:#ffffff background-color:#000000">
Title
</h1>
<div class="font-size:16px">
<mt:EntryBody />
<mt:EntryMore />
</div>
</body>
</html>
view raw test.mtml.tsx hosted with ❤ by GitHub

このMTMLのclassを解析して、MasterCSSを使って以下のCSSを出力することができます。

.background-color\:\#000000{background-color:#000000}.color\:\#ffffff{color:#ffffff}.font-size\:16px{font-size:16px}.font-size\:24px{font-size:24px}.font-weight\:bold{font-weight:700}
view raw style.css hosted with ❤ by GitHub

フロントエンドに関しては、2023年においてはウェブサイトを運用する際には何らかのビルド作業が入っていることが多いと思いますが、MTMLや(MTMLの内容を踏まえた上での)CSSもビルドするようになってもいいのかもしれません。MTのプラグイン(主にPerl)でビルドできればよいようにも思いますが、プラグインだとサーバー上で動作させることになり制限があることも多いので、作業環境の方が自由に環境を作れるというのもあり、これはこれでよいアプローチだと思います。

「ここまでやるならJamstackで生成した方が素直じゃないか?」という指摘があっても否めない気もします。とはいえまあ、上と同じ理由で運用環境は自由にならないこともあるのでMTを使ううえではMTMLの開発効率が上がるのはよいことだと思います。

プレビューもしたい

「JSXでパースできる」ということは、mt:Entriesやmt:EntryTtileのモックを作ってあげれば、JSXを直接文字列化してテンプレートの出力結果も確認できそうです。これになってくると上で出たmt-data-api-reactとも近い話になってくるのですが。これをやるには時間が足りなかったのでやっていませんが、後はモックを頑張って作りさえすれば、プレビューできるというところまで来ていると思います。

今回作成したもの

mt-jsx-templateです。

$ npx mt-jsx-template ファイル名

だけですぐに動かせます。また標準入力からも読み込めますので、動くことだけ確認したい場合には以下のコマンドを実行すれば変換された結果を見ることができます。

$ curl https://gist.githubusercontent.com/usualoma/e3d68e800bd264e9cdb0f2b7b1b19a64/raw/7c0b05a92e95f01f4e317d8c7ab46cb118296b66/entry.mtml.tsx | npx mt-jsx-template

TSXを書いて実際に型の付くことを確認したい場合には以下のようにプロジェクトにインストールして、tsconfig.jsonを設定してもらえば好きなエディタで動作させることができます。

$ npm install --save-dev mt-jsx-template

属性値の型も定義されているので、補完が効いたり、マニュアルを参照できたりします。

これは、結構いいんじゃないでしょうか!

この記事について

この記事は Movable Type Advent Calendar 2023 の25日目の記事です。皆様お疲れさまでした。それでは良いお年を!

honoのJSXの特徴

  • 投稿日:
  • by
  • カテゴリ:

これはなんの記事か?

honoはJSXを処理する実装をもっています。JSXはJavaScriptの構文を拡張したもので...のような話は皆さんご存知でしょうしググってもらった方が正確な情報を得られると思うのでここでは書かずに、honoの実装での特徴的なところについて書いていこうと思います。

特徴は?

文字列の出力に特化していることです。Reactでいうところの`renderToString(<App />)` のみに対応しています。`<App />.toString()` が文字列になります。`document.body.innerHTML = <App />`が動くのはそのためです。

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を返すようになってしまう気持ち悪さはありますが、受け取る側でラップすれば通常の文字列と同じ感覚で使うことができます。

const Content = async () => {
const data = await fetch('https://ramen-api.dev/shops/takasagoya').then((res) => res.json())
return <div>{data.shop.name}</div>
}
return () => (
<html>
<body>
<Content />
</body>
</html>
)
view raw page.tsx hosted with ❤ by GitHub

これは実はReactとも互換性があって、18.2まではエラーになるのですが、Reactの18.3(2023年12月現在は@nextのステータス)では`renderToReadableStream`を使うことで文字列にすることができます。Suspenseも不要です。

完全な互換性があるわけではないのであまり不用意なことは言えないのですが、honoのJSXは独自の実装ではあるものの意外と本家の挙動も意識しており、後述するSuspenseにおいても相互の乗り換えで混乱することがないようにしています。

もちろんですが、返されたPromiseは並列で処理されるので、同時にたくさんの非同期なコンポーネントを使っても大丈夫です。

const Content = async ({id}) => {
const data = await fetch(`https://ramen-api.dev/shops/${id}`).then((res) => res.json())
await new Promise((resolve) => setTimeout(resolve, 1000))
return <div>{data.shop.name}</div>
}
return () => (
<html>
<body>
<Content id="yoshimuraya"/>
<Content id="sugitaya"/>
<Content id="takasagoya"/>
<Content id="jyoujyouya"/>
<Content id="torakichiya"/>
</body>
</html>
)
view raw page.tsx hosted with ❤ by GitHub

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の完了後に内容が更新されるという動作になります。

const Content = async () => {
const data = await fetch('https://ramen-api.dev/shops/takasagoya').then((res) => res.json())
return <div>{data.shop.name}</div>
}
return () => (
<html>
<body>
<Suspense fallback={<div>Loading...</div>}>
<Content />
</Suspense>
</body>
</html>
)
view raw page.tsx hosted with ❤ by GitHub

ErrorBoundary

ErrorBoundaryはReactの本体では提供されていないのですが、bvaughn/react-error-boundaryという公式のドキュメントからもリンクされていて広く使われているライブラリがあり便利そうだったので、仕様を真似て実装しました。

以下のコードで、最初に「Loading...」が表示され、その後例外が投げられたタイミングで「Something went wrong」に更新されるという動作になります。

const Content = async () => {
const data = await fetch('https://ramen-api.dev/shops/takasagoya').then((res) => res.json())
throw new Error()
return <div>{data.shop.name}</div>
}
return () => (
<html>
<body>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<Content />
</Suspense>
</ErrorBoundary>
</body>
</html>
)
view raw page.tsx hosted with ❤ by GitHub

ErrorBoundary[onError]やErrorBoundary[fallbackRender]を使って、ログを記録したり、エラーの内容に応じて出力を切り替えることもできます。

<html>
<body>
<ErrorBoundary fallback={<Fallback />} onError={(e) => console.log(e)}>
<Suspense fallback={<div>Loading...</div>}>
<Shop id='takasagoya' />
</Suspense>
</ErrorBoundary>
</body>
</html>
view raw page.tsx hosted with ❤ by GitHub
<html>
<body>
<ErrorBoundary
fallbackRender={(e: Error) => <div>Got an error: {e.message}</div>}
>
<Suspense fallback={<div>Loading...</div>}>
<Shop id="takasagoya" />
</Suspense>
</ErrorBoundary>
</body>
</html>
view raw page.tsx hosted with ❤ by GitHub

以上、honoのJSXの実装の紹介でした。

これは Hono Advent Calendar 2023 の13日目の記事です。引き続きよろしくお願いします!