先週のRemix 3のFrameを使いジャンケンアプリを動かしました、苦労したけど面白い!の続きです。
前回の説明は個々のコードの解説だったので、Remix3やFrame全体の流れは判りにくかったと思い。今回は図を書きRemix3のFrameの動作を解説しました。
Bing Image Creatorが生成した画像を使っています
開発環境
本題に入る前に、前回は開発環境の説明をしてなかったので簡単に説明します。package.jsonのscriptには以下の2つのスクリプトが登録されています。
devは開発用サーバーをtsxコマンドで起動してTypeScript→JavaScript変換を行ってサーバーサイドのコードを実行しています。
dev:browserはクライアントサイドで実行されるJavaScriptの生成(生成されたJavaScriptはpublic/assets/に置かれブラウザーからアクセスされます)を行います。
どちらも起動しておく必要があります。またHMR(ホットリロード)はサポートされてないのでコードを変更した際にはブラウザーのリロード・ボタンを押す必要があります、現在の開発環境は最低限の実装といったレベルです。
- package.json
"scripts": {
"dev": "NODE_ENV=development tsx watch server.ts",
"dev:browser": "esbuild app/assets/*.tsx --outbase=app/assets
--outdir=public/assets --bundle --minify --splitting --format=esm
--entry-names='[dir]/[name]' --chunk-names='chunks/[name]-[hash]'
--sourcemap --watch"
・・・
}
Remix3のFrame処理の流れ
本題のRemix3のFrame処理の流れを説明します、以下の画像はデベロッパーツールでブラウザーのネットアクセスを表示したものです。
オレンジ色の点線の上は初期表示時のネットアクセスで、点線の下はジャンケンボタンを押した際のネットアクセスです。
browser@4はTailwind CSSの取得なので無視してください。

初期表示(1)
Remix3はサーバーサイドレンダリング(SSR)のフレームワークなので/へのGETリクエストがサーバーに届くと、サーバーサイドのapp/jyanken.tsxが動き(レンダリングされ)、ここから呼び出されるる複数のコンポーネントがレンダリングされてHTMLが生成されます。クライアントサイドで動作するte-button.tsxもいったんサーバーサイドでレンダリングされます、当然イベントハンドラー等のJavaScriptのコードは含まれません。
また、重要な点は/assets/entry.jsがブラウザーにロード&実行される事です。
- app/jyanken.tsx
サーバーサイドのルーティングに対応したジャンケンアプリ用の特殊なコンポーネントです。
・・・
export default {
middleware: [],
handlers: {
index() {
return render(
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" async src={routes.assets.href({ path: "entry.js" })} />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<title>Jyanken</title>
</head>
<body>
<div class="mx-8 w-1/2">
<h1 class="my-6 text-center text-xl font-bold">
ジャンケンポン
</h1>
<Frame src={routes.fragments.jyankenMain.href()} />
</div>
</body>
</html>
)
},
・・・
}
} satisfies RouteHandlers<typeof routes.jyanken>
app/jyanken.tsxから呼び出されるコンポーネント
- app/components/jyanken-main.tsx
- app/components/jyanken-box.tsx
- app/components/score-list.tsx
- app/assets/te-button.tsx
生成されたHTML
ここで生成されたHTMLの一部を載せます。
- ① クライアントサイドで最初に動くJavaScriptです
- ② ここがFrameの始まりの目印です
- ③ クライアントサイドのコンポーネントの始まりの目印?
- ④ コンポーネントをハイドレーションするための情報です
- コンポーネントのURLや名前、引数(Props)等が書かれています
- ⑤ Frameの終わりの目印と、Frameを読み込むためのJSON情報
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" async src="/assets/entry.js"></script> <!-- ← ① -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<title>Jyanken</title>
</head>
<body>
<div class="mx-8 w-1/2">
<h1 class="my-6 text-center text-xl font-bold">ジャンケンポン</h1>
<!-- frame:start:f1 --> <!-- ← ② -->
<div class="w-[270px] mx-auto flex justify-center mb-10">
<div class="flex-1">
<!-- rmx:h --> <!-- ← ③ -->
<form method="POST" action="/">
<button
type="submit"
class="text-white text-center text-sm rounded w-16 px-2 py-2 bg-blue-600 hover:bg-blue-700 shadow shadow-gray-800/50"
style="opacity: 1"
>
グー</button
><input type="hidden" name="human" value="0" />
</form>
<!-- /rmx:h --> <!-- ↓ ④ -->
<script type="application/json" rmx-hydrated>
{
"moduleUrl": "/assets/te-button.js",
"exportName": "TeButton",
"props": { "value": 0 },
"id": "h1.1"
}
</script>
</div>
・・・
<!-- frame:end:f1 --> <!-- ↓ ⑤ -->
<script type="application/json" rmx-frame="f1">
{
"id": "f1",
"status": "resolved",
"src": "/fragments/jyanken-main"
}
</script>
</div>
</body>
</html>
初期表示(2)
jyanken.jsのコードが実行されハイドレーションが開始されます。ここでRemix3のランタイムライブラリーが読み込まれ、そのランタイムライブラリーがクライアントサイドのコンポーネントte-button.jsを読み込み、HTML上のボタンへのイベントハンドラー等が設定されます。その際には上の④で説明したJSON情報にあるPropsが使われます。
- app/assets/entry.tsx
このコードはランタイムライブラリーから呼び出されるJavaScriptモジュールのローダーです。特別な処理を組み込めるようにアプリケーション側にあるそうです。
import { createFrame } from '@remix-run/dom'
createFrame(document, {
async loadModule(moduleUrl, name) {
let mod = await import(moduleUrl)
if (!mod) {
throw new Error(`Unknown module: ${moduleUrl}#${name}`)
}
let Component = mod[name]
if (!Component) {
throw new Error(`Unknown component: ${moduleUrl}#${name}`)
}
return Component
},
・・・
})
ジャンケンボタンを押した(1)
ジャンケンボタンを押すと、下に示したイベントハンドラーが実行されます。 この中でfetch(action, { method, signal, body })が実行され、サーバーに POST / が送られます。パラメーターとして{human: 2}のような人間が押したボタンの情報も送られサーバーサイドでジャンケンが実行され、ジャンケンの結果がサーバーサイドのモデルに格納されます。
- app/assets/te-button.tsx
イベントハンドラーの部分のみ書きだしました。
<form method={method} action={action}
on={dom.submit(async (event, signal) => {
event.preventDefault()
const body = new FormData(event.currentTarget)
posting = true
this.update()
await fetch(action, { method, signal, body }) // ← (1)
if (signal.aborted) return
await this.frame.reload() // ← (2)
if (signal.aborted) return
posting = false
this.update()
})}>
<button type="submit" class={buttonClass} style={{opacity: posting ? 0.5 : 1.0}}>
{TeString[value]}
</button>
<input type="hidden" name="human" value={value.toString()} />
</form>
- app/jyanken.tsx
サーバーサイドではこのリクエストを受け付け、pon関数が実行されます。
・・・
export default {
middleware: [],
handlers: {
・・・
async pon({ formData }) {
createScore(Number(formData.get('human')?.toString() ?? '0') as Te)
return new Response(null, { status: 204 })
}
}
} satisfies RouteHandlers<typeof routes.jyanken>
ジャンケンボタンを押した(2)
イベントハンドラー内のthis.frame.reload()が実行されるとランタイムライブラリーを経由して、entry.jsのresolveFrame関数が呼び出されます。
resolveFrameは、⑤のJSONデータをもとにサーバーサイドの /fragments/jyanken-mainにリクエストが送りapp/fragments.tsxが起動されます。
そこで<JyankenMain />コンポーネントがレンダリングされ生成されたHTML片が戻されます。
戻ったHTML片をランタイムライブラリーが、画面のうちFrameの内部を書きかえます。
- app/assets/entry.tsx
このコードはランタイムライブラリーから呼び出され、サーバーサイドのFrameのレンダリングを呼出す関数です。特別な処理を組み込めるようにアプリケーション側にあるそうです。
import { createFrame } from '@remix-run/dom'
createFrame(document, {
・・・
async resolveFrame(frameUrl) {
let res = await fetch(frameUrl)
if (res.ok) {
return res.text()
}
throw new Error(`Failed to fetch ${frameUrl}`)
},
})
- app/fragments.tsx
サーバーサイドのルーティングに対応したFrameの用の特殊なコンポーネントです。
export default {
middleware: [],
handlers: {
jyankenMain() {
return render(<JyankenMain />)
},
},
} satisfies RouteHandlers<typeof routes.fragments>
まとめ
Remix3のFrameの解説をしました、このようにRemix3ではサーバーサイドレンダリング+ハイドレーションやFrameによる部分書き変えの動作が透けて見えています。 ここが高機能ですが全く内部が見えないNext.jsとの違いだと思います。
Remix3ではNext.jsに比べてたくさんのコードを書かないと行けません、また現時点では便利なサーバー・ファンクションもありません。
しかし、そこで得られるメリットは高いパフォーマンスでしょうか。
まず仮想DOMのような大きなランタイムライブラリーは有りませんし、動きが見えるということはパフォーマンス・チューニングが行いやすいということです。
以前取り上げたTanStack StartもRemix3と同様に透明性の高いフレームワークで、最近話題になってきています。
Next.jsが、一強のReact界にも変化があらわれるのでしょうか。










