自分用の管理画面(サーバーで描画した React のページ)で、項目を書き換えて「保存」を押しました。画面は一瞬読み込み直したように見えて、エラーも出ません。ところが一覧に戻ると何も変わっていませんでした。2回やって2回とも空振りでした。
アドレスバーを見ると、URL の後ろに入力した値が付いていました。
https://example.com/admin/items/123?name=新しい名前&status=paused
原因
フォームの保存処理は onSubmit で preventDefault() してから fetch で送る作りでした。
<form onSubmit={async (e) => {
e.preventDefault();
await fetch(`/api/items/${id}`, { method: "PATCH", body: new FormData(e.currentTarget) });
}}>
この onSubmit が効くのは、ブラウザで JavaScript が読み込まれて React が画面に結び付いた(ハイドレーションが終わった)あとです。それより前に押すと、HTML の <form> として素のまま送信されます。method も action も書いていないので、同じ URL への GET になり、値はクエリ文字列に付くだけでサーバーには保存されません。
私の場合は、Chrome のウィンドウが裏にあってタブが非表示(document.visibilityState === "hidden")だったため、読み込みが遅れて押す方が先になっていました。ブラウザ操作を自動化していて、画面を開いた直後に押していたのも原因です。
直し方1:押す前に React が結び付いたかを確かめる
自動化で押すなら、フォーム要素に React のプロパティが付いたのを待ってから押します(React の内部の名前なので、確認用と割り切ります)。
const ready = () =>
Object.keys(document.querySelector("form")).some((k) => k.startsWith("__reactProps"));
直し方2:入力は React の状態に入ったかも確かめる
制御コンポーネントの入力欄は、el.value = "..." で書き換えても React の状態には入りません。ネイティブの setter を呼んでから input イベントを出します。
const el = document.querySelector('input[name="name"]');
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set;
setter.call(el, "新しい名前");
el.dispatchEvent(new Event("input", { bubbles: true }));
入力した値で表示が変わる箇所(プレビューなど)があれば、それが変わったのを見てから保存します。
直し方3:保存できたかは画面遷移ではなく一覧で見る
今回は「画面が読み込み直した=保存できた」と思い込んだのが空振りに気づけなかった理由でした。保存後は一覧や詳細を開き直して、値が変わっているかで判断します。
作る側としては、<form method="post"> にしておくか、読み込みが終わるまで保存ボタンを disabled にしておけば、GET で値が URL に出ることは防げます。
まとめ
- サーバーで描画した React のフォームは、読み込み前に押すと素の HTML フォームとして送信される
-
methodが無ければ GET になり、値は URL に付くだけで保存されない。エラーも出ない - 押す前に React が結び付いたかを確かめ、保存できたかは一覧で見直す
生成AI(Claude Code)に書かせて一人で会社を回している記録を note に書いています。
https://note.com/kaito_cometa