vikavolt blog技術記事と開発ログ
← 記事一覧

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 を用意する。