React Router v7で画像アップロード後に画面が固まった
管理画面の画像アップロードが固まる問題は、fetcher.Form と data() を使う形に直しました。
サーバー側の R2 への保存と D1 への記録は成功しており、問題は送信方法と応答形式の組み合わせにありました。
私が踏んだリソースルートへの POST の落とし穴と、ローカル検証で詰まった点をまとめます。
保存は成功しているのに、画面が進まない
管理画面で画像を選び、「アップロード」を押すと画面が固まりました。一方、サーバー側では R2 に画像が保存され、D1 にも記録されていました。保存処理の成功だけでは、画面操作が完了したとは判断できない状態です。
送信には、次のような <Form> を使っていました(実装イメージです。実際のコードから属性を簡略化しています)。
<Form
method="post"
action="/admin/upload"
encType="multipart/form-data"
>
<input type="file" name="file" />
<button type="submit">アップロード</button>
</Form>
送信先の /admin/upload は、コンポーネントを持たないリソースルートです。その action は、text/plain の生の Response を返していました。
この構成では、クライアントのルーターが送信をナビゲーションとして扱い、single-fetch の /admin/upload.data にリクエストを投げます。しかし、返ってくる応答を所定の形式として解釈できませんでした。
今回の原因は、保存処理の後にありました。「POST が成功したか」に加えて、「ルーターがその応答を受け取れるか」まで見る必要がありました。
画面内で完結する操作を fetcher.Form にする
アップロード後に必要なのは、同じ管理画面で結果を受け取り、本文に画像を追加することです。そこで、useFetcher の fetcher.Form で送信する形に変更しました。
action 側も、生のテキストを返す形から data({ ok, url, markdown }) を返す形に変更しています。送信方法と返却形式をそろえ、画面で結果を扱えるようにしました。
画面側では fetcher.state を使って送信中であることを表示します。成功後には、次の要素を出します。
- アップロードした画像のサムネイル
- コピーして貼り付けられる Markdown
- 「本文の末尾に挿入」ボタン
検証エラーは、data({ ok: false, error }, { status: 400 }) で返します。また、内部エラーの文言をそのまま表示せず、日本語の固定メッセージに写像しました。
成功時だけでなく、失敗時にも画面が受け取るデータの形を決めておくことが、この修正のポイントです。
textarea への挿入は変更通知まで通す
「本文の末尾に挿入」では、uncontrolled な textarea に外から値を入れる必要がありました。ここでは、値の書き換えと React の onChange への通知をつなぐために、ネイティブの value セッターを使っています。
要点を抜き出すと、次のような関数です。
function appendMarkdown(
textarea: HTMLTextAreaElement,
markdown: string,
) {
const setter = Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
"value",
)?.set;
if (!setter) return;
const separator = textarea.value ? "\n\n" : "";
setter.call(textarea, textarea.value + separator + markdown);
textarea.dispatchEvent(new Event("input", { bubbles: true }));
}
HTMLTextAreaElement.prototype のセッターで値を更新した後、バブリングする input イベントを送ります。これによって、挿入した値を既存の変更処理にも渡します。
この方法を持ち帰る場合は、文字が表示されたことに加えて、onChange 側の処理まで動くことを確認すると安心です。
ローカル検証ではホストと D1 の ID に注意する
修正を確かめる際には、ローカル環境でも詰まりました。
wrangler.jsonc にカスタムドメインの routes があると、今回の環境では wrangler dev の request.url のホストが本番ドメインになりました。そのため、localhost 限定の開発用バイパスが効きませんでした。
次のオプションを付けて回避しました。
wrangler dev --host 127.0.0.1 --upstream-protocol http
ローカルで起動したつもりでも、アプリケーションが判定に使うホストが何かは確認が必要です。
もう一つの注意点は、ローカル D1 が database_id ごとに別物になることです。ID を変更したら、変更後のローカル D1 にマイグレーションを当て直す必要があります。ホストの判定とデータベースの状態は、それぞれ確認する項目として扱います。
学び:ログイン後の操作まで検証する
今回の問題は、ユニットテストと 2 周のレビューでは見つからず、初回の手動操作で発覚しました。私にとって、次に補うべき検証がはっきりした出来事でした。
- 画面内で完結する送信では、ナビゲーションが必要かを考え、送信方法と action の返却形式を合わせる。
- 保存成功だけで完了とせず、送信中表示、結果表示、エラー表示まで確認する。
- textarea に外から値を入れる場合は、表示と変更通知の両方を確認する。
- ローカル検証では
request.urlのホストとdatabase_idを確認し、ID 変更後はマイグレーションを当て直す。 - ログイン後に画像を選び、アップロード結果を受け取り、本文に挿入するところまで通す E2E を用意する。