何が起きているか
React で入力フォームを作って、送信ボタンを押した瞬間に
- 画面が一瞬白くなる(リロードされる)
- 入力した値が全部消える
-
console.logに出したはずのログも消える
という現象があります。AI にコードを書いてもらった直後に気づきやすいので「AI が変なコードを書いた」と疑いたくなりますが、AI が的外れなものを書いたわけではありません。原因の場所が違うだけで、足りないのは1行です。
起きているのは、<form> の既定動作です。
なぜリロードされるのか
<form> は React が作った仕組みではなく、HTML にもともとある要素です。そして HTML のフォームには、送信されたら action 属性の URL にリクエストを送ってページを遷移する、という既定の動作があります。
action を書いていない場合、送信先は現在のドキュメントの URL になります。さらに method の既定値は GET なので、実際には現在のパスの末尾に ? と入力値が付いた URL が読み込み直されます。
見分け方として使えます。送信ボタンを押したあと、アドレスバーの URL に ? が増えていたら、フォーム送信が起きたサインです。入力欄に name 属性が付いていれば ?name=taro のように入力値そのものが URL に出ます。
なお、もともと URL に付いていたクエリ文字列は、フォームの内容で置き換わります。?case=4 を開いた状態で name="a" の欄に taro と入れて送信すると、URL は ?a=taro になります。
そして React のコンポーネントはページが読み込み直されると初期状態に戻るので、useState に入れていた入力値も一緒に消えます。「入力が消えた」の正体はこれです。
ℹ️ React 19 以降で
<form action={関数}>のようにactionに関数を渡す場合(Server Actions など)は、React 側が既定動作を止めてくれるのでpreventDefault()は要りません。この記事はonSubmitで処理する従来の書き方の話です。
再現コード
"use client";
import { useState } from "react";
export default function ContactForm() {
const [name, setName] = useState("");
const handleSubmit = () => {
console.log("送信:", name); // ← 出るがすぐ消える
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit">送信</button>
</form>
);
}
これを動かすと、handleSubmit はちゃんと呼ばれます。呼ばれたうえで、そのあとブラウザがフォームの既定動作を実行してページを再読み込みします。
「関数が呼ばれていないのでは」と疑って console.log を増やしていくと、ログが一瞬出て消えるので余計に混乱します。ここが引っかかりやすいところです。
💡 DevTools の Console で Preserve log にチェックを入れると、リロードをまたいでログが残ります。「呼ばれていない」のか「呼ばれたあとに消えている」のかを、これで切り分けられます。
直し方
イベントオブジェクトを受け取って、preventDefault() を呼びます。
import type { FormEvent } from "react";
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); // ← 既定のページ遷移を止める
console.log("送信:", name);
};
preventDefault() は「このイベントに紐づいたブラウザの既定動作をやらせない」という指示です。フォーム送信の場合は、ページ遷移がその既定動作にあたります。
これでリロードが止まり、name の値も残ります。
type="button" で消してはいけない理由
検索すると「<button type="button"> にすればリロードされない」という対処も出てきます。クリックしたときのリロードは、たしかに止まります。
{/* 症状は消えたように見えるが、副作用がある */}
<button type="button" onClick={handleClick}>送信</button>
ただしこれは、ボタンを押してもフォームを送信しない状態にしているだけです。次の2つが変わります。
1. ブラウザ標準のバリデーションが走らなくなる
クリック経路では送信そのものが起きないので、required や type="email" を付けても検証されないまま onClick の処理が動きます。「required を付けたのに未入力で通ってしまう」の原因がこれだったりします。
2. Enter キーの挙動が、テキスト入力欄の数によって変わる
ここが分かりにくいところです。HTML には**暗黙の送信(implicit submission)**という仕様があり、入力欄で Enter を押したときの挙動が次のように分かれます。
| フォームの状態 | Enter を押すと |
|---|---|
| submit ボタンがある | そのボタンがクリックされる=送信される |
| submit ボタンが無く、テキスト入力欄が1つだけ | フォームが送信される(=リロードが再発する) |
| submit ボタンが無く、テキスト入力欄が2つ以上 | 何も起きない |
⚠️ ここで数えるのは <input type="text"> や type="email" などの1行テキスト入力だけです。<textarea> や <select>、チェックボックスは数に入りません。
これが地味に効きます。たとえば「名前(input)+ お問い合わせ内容(textarea)」という、いちばんありがちなフォーム。見た目は入力欄が2つですが、数えられるのは input の1つだけなので、上の表の2行目が適用されます。つまり名前欄で Enter を押すとリロードが再発します(textarea の中で押した Enter は改行になるだけなので、そちらで試すと何も起きません)。
つまり type="button" にすると、クリックでは直ったように見えるのに、Enter を押した人にだけ再発するという状態が作れてしまいます。これが見つけにくさの正体です。
どちらにしても、送信の意味を持たせたいなら type="submit" のまま onSubmit で preventDefault() するのが基本です。
ℹ️ ちなみに
<button>はtypeを省略すると submit 扱いになります。フォームの中で送信以外の用途にボタンを置くときは、明示的にtype="button"を書く必要があります(今回とは逆に、書かないと送信されてしまいます)。
<a> タグでも同じことが起きます
preventDefault() はフォーム専用の話ではありません。既定動作を持つ要素すべてが対象です。
// クリックすると handleClick は動くが、そのあとページ最上部にジャンプする
<a href="#" onClick={handleClick}>詳細</a>
href="#" の既定動作は「文書の先頭へのスクロール」なので、位置が飛びます。加えて履歴に1件積まれるので、戻るボタンを押したときの挙動も変わります。ここでも e.preventDefault() が要りますが、そもそもページ遷移しないなら <button> を使うほうが適切です。
AI に聞くときの伝え方
この症状は「エラーが出ない」のが特徴です。コンソールは赤くならず、ただリロードされるだけなので、エラー文を貼って聞くことができません。
なので、症状をそのまま言葉にして渡します。
Next.js (App Router) + React で入力フォームを作っています。
送信ボタンを押すと handleSubmit の console.log は一瞬出るのですが、
直後にページがリロードされて入力値が消えます。エラーは出ていません。
現在のコードは以下です。
(コードを貼る)
原因と、type="button" 以外の直し方を教えてください。
ポイントは2つです。
- 「エラーは出ていない」と明示する。書かないと、AI は存在しないエラーを前提に推測を広げがちです
-
type="button"以外で、と条件を付ける。これを書かないと、症状だけ消える対処が返ってくることがあります
まとめ
- 送信でリロードされるのは React のバグではなく
<form>の既定動作(methodの既定が GET なので URL に?が付く) -
onSubmitの第1引数を受け取ってe.preventDefault()を呼ぶ -
type="button"は標準バリデーションを失わせ、Enter の挙動もテキスト入力欄の数で変わる(1つだけならリロードが再発する/textarea や select は数に入らない) -
<a href="#">でも同じ既定動作の問題が起きる(先頭スクロール+履歴が積まれる) - エラーが出ない症状は、症状の言葉と「エラーは出ていない」をセットで AI に渡す
この記事は初心者向けの内容です。中上級者の方には易しすぎる内容だと思いますので、プログラミングを始めたばかりの知り合いへの紹介や、社内研修の参考としてお使いいただければ嬉しいです。
未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまでを教材にまとめています。
- ▶ ライブデモ(インストール不要・無料)→ https://trial-web-ayies128s-projects.vercel.app/?ref=qiita
- 教材完全版+チャットサポート(月5,500円)→ https://menta.work/plan/20251?ref=qiita
- 体験版(Session 0〜3・無料)→ https://github.com/ayies128/next-ai-camp-trial
- YouTube『AIエンジニア情報局』(AI×開発ニュースを1本5分でキャッチアップできる別運営チャンネル・無料)→ https://www.youtube.com/channel/UC1rXVD9WYsQPQEWZyd-A1KA/?ref=qiita