Reactでformの中のbuttonを押すと送信される理由
<form> の中に置いた <button> に type を書かないと、HTML の決まりでは送信ボタンとして扱われます。React は type を補わずにそのまま HTML に出すので、押すとフォームが送信され、送信先を決めていないフォームは今のページを読み込み直します。ログインフォームの「パスワードを表示」ボタンを押しただけで、ページが読み込み直されて入力が消えるときは、この仕組みが原因です。送信しないボタンに type="button" を書けば、送信されなくなります。
私は検証環境で、type の書き方を変えた「表示」ボタンを作り、何が起きるかを Chromium で確かめました。直し方とあわせて、type を書いたつもりでも送信ボタンのままになる書き方と、送信したボタンを突き止める方法も紹介します。
検証環境は Ubuntu 24.04、Node.js 22.23.4 です。react / react-dom 19.3.1、@types/react 19.3.0、TypeScript 6.0.3、esbuild 0.28.2、ESLint 9.39.5、eslint-plugin-react 7.37.5、typescript-eslint 8.72.2 を使いました。.tsx を esbuild で開発用にまとめ、Playwright 1.63.0 で動かした Chromium 153 で操作しました。StrictMode、Vite などのビルドツールや Next.js などのフレームワーク、Chromium 以外のブラウザーでは試していません。

type を書かない button は、HTML では送信ボタン
HTML の仕様にある、button 要素の type 属性の説明では、type には submit(送信する)、reset(入力を初期値に戻す)、button(何もしない)の3つの値があります。type を書かなかったときと、決められた3つ以外の値を書いたときは、どちらも Auto という状態になります。Auto の button は、command 属性・commandfor 属性がなく、select 要素の直下でもなければ、送信ボタンとして扱われます。
MDN の button 要素の type 属性の説明でも、フォームに関連付けられたボタンで type がないときや、空や不正な値のときは submit になると書かれています。
これは、ブラウザーで要素を調べると確かめられます。次の NoType の「表示」ボタンについて、getAttribute('type') と type プロパティを読みました。
import { useState } from 'react';
export function NoType() {
const [show, setShow] = useState(false);
return (
<form>
<input name="email" aria-label="メールアドレス" />
<input name="password" type={show ? 'text' : 'password'} aria-label="パスワード" />
<button onClick={() => setShow(!show)}>表示</button>
</form>
);
}
type 属性は null(書かれていない)でしたが、type プロパティは "submit" を返しました。仕様の type プロパティの説明にも、送信ボタンなら "submit" を返すとあります。type 属性を書いていなくても、type プロパティを読めば、そのボタンが送信ボタンかどうかが分かります。
React は type を補わない
React が出す HTML にも、type は付きません。react-dom/server の renderToStaticMarkup で、type を書いた場合と書かない場合を文字列にしました。
import { renderToStaticMarkup } from 'react-dom/server';
console.log(renderToStaticMarkup(<button>表示</button>));
console.log(renderToStaticMarkup(<button type="button">表示</button>));
<button>表示</button>
<button type="button">表示</button>
ブラウザーで描画した NoType のボタンも、outerHTML は <button>表示</button> でした。TypeScript の型(@types/react)でも、button の type は "submit" | "reset" | "button" | undefined で、書かなくてもエラーになりません。React の書き方が原因というより、HTML の button をそのまま使っているために起きることです。
送信がページの再読み込みになるまで
仕様の button 要素の説明では、押されたボタンがフォームに属していて、かつ送信ボタンなら、そのフォームを送信すると決めています。フォーム送信の手順では、送信先(action)が空なら、今のページの URL を送信先にします。MDN の form 要素の method 属性の説明によると、method の既定は GET で、フォームのデータは URL の ? のあとに付きます。
NoType で、メールアドレスとパスワードを入れて「表示」を押した結果です。
押す前: URL=/NoType / 読み込んだ回数:1 / パスワード欄 type=password 値=「nekoneko」
「表示」を押したあと: URL=/NoType?email=tama%40example.com&password=nekoneko / 読み込んだ回数:2 / パスワード欄 type=password 値=「」
ページを読み込んだ回数が増え、パスワード欄は空になりました。show の state も初めの false に戻ったので、パスワード欄は伏せ字のままです。
この結果のとおり、type を書き忘れた「表示」ボタンを押しただけで、入力したパスワードがページの URL に入りました。
type を書いたつもりでも送信ボタンのままになる書き方
値を書きまちがえた
type="buton" のように値をまちがえると、仕様でいう不正な値にあたり、書かなかったときと同じく送信ボタンになります。検証でも、type プロパティは "submit" を返し、押すと URL が /Typo?password=nekoneko になって読み込み直されました。
ただし、tsc で型チェックをすれば、ビルドの前に気づけます。
typo/typo.tsx(9,15): error TS2820: Type '"buton"' is not assignable to type '"button" | "reset" | "submit" | undefined'. Did you mean '"button"'?
esbuild は型を調べずに変換するので、この状態でもビルドは通りました。型チェックを別に走らせていないと、見逃します。
自作の Button コンポーネントが type を受け取らない
見た目をそろえるために、button を包んだコンポーネントを作ることがあります。
import type { ComponentProps } from 'react';
function Button(props: ComponentProps<'button'>) {
return <button className="btn" {...props} />;
}
// 使う側
<Button onClick={() => setShow(!show)}>表示</Button>
使う側が type を渡さなければ、中の button にも type は付きません。検証では NoType と同じく送信ボタンになり、押すと URL が /WrapperNoType?password=nekoneko になって読み込み直されました。
form の外に置いて、form 属性で結びつけた
<form> の外にある button は、フォームに属さないので、押しても何も送信しません。検証でも、外に置いたボタンは押しても URL は変わらず、入力も残りました。
ところが、MDN の button 要素の form 属性の説明にあるとおり、form="search" のようにフォームの id を書くと、外にあってもそのフォームに属します。type がなければ送信ボタンなので、押すと URL が /Outside?q=たま になって読み込み直されました。
送信しないボタンには type="button" を書く
直し方は、送信しないボタンに type="button" を書くことです。
<button type="button" onClick={() => setShow(!show)}>表示</button>
検証では、押しても URL と読み込んだ回数は変わらず、パスワード欄が type=text になって「nekoneko」が見えるようになりました。type プロパティも "button" を返しました。
自作の Button コンポーネントは、type を渡さなければ "button" になるようにしておくと、書き忘れても送信されません。
import type { ComponentProps } from 'react';
// type を渡さなければ "button" になる Button コンポーネント("submit" のときだけ送信ボタン)
function SafeButton({ type, ...props }: ComponentProps<'button'>) {
return <button className="btn" type={type === 'submit' ? 'submit' : 'button'} {...props} />;
}
// 使う側
<SafeButton onClick={() => setShow(!show)}>表示</SafeButton>
<SafeButton type="submit">ログイン</SafeButton>
この形では、type="reset" は使えません。リセットボタンが要らないフォームのための書き方です。検証では、「表示」は押してもパスワードが見えるようになるだけで、onSubmit は呼ばれませんでした。「ログイン」を押したときだけ、onSubmit(preventDefault を呼んでメッセージを出すもの)が動きました。
なお、type を書かずに、onClick の中で e.preventDefault() を呼ぶ形でも、検証では送信されませんでした。ただ、この形ではボタンの type プロパティは "submit" のままで、後の節で紹介する ESLint のルールでもエラーになりました。
どのボタンが送信したかを調べる
フォームにボタンがいくつもあり、どれが送信を起こしているのか分からないときは、onSubmit で受け取ったイベントの nativeEvent.submitter を見ます。MDN の SubmitEvent.submitter の説明のとおり、送信を起こしたボタンが入っています。
import type { SubmitEvent } from 'react';
function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
e.preventDefault();
const submitter = e.nativeEvent.submitter;
console.log('送信したボタン:', submitter?.textContent, '/ e.submitter:', e.submitter?.textContent);
setMessage('ログインします');
}
「表示」と「ログイン」の2つのボタンを type なしで置き、「表示」を押すと、コンソールに次のように出て、「ログインします」とも表示されました。
送信したボタン: 表示 / e.submitter: 表示
type を書き忘れた「表示」ボタンが、ログインの処理まで動かしていたことが分かります。検証環境(react-dom 19.3.1、@types/react 19.3.0)では、nativeEvent を通さず e.submitter と書いても、同じボタンを取れました。
ESLint の react/button-has-type で書き忘れを見つける
eslint-plugin-react には、type のない button を見つける react/button-has-type というルールがあります。
// eslint.config.js
import react from 'eslint-plugin-react';
import tseslint from 'typescript-eslint';
export default [
{
files: ['src/**/*.tsx'],
languageOptions: { parser: tseslint.parser },
plugins: { react },
rules: { 'react/button-has-type': 'error' },
},
];
この記事の例をまとめたファイルに使うと、type のない button の行が、次のようにエラーになりました(作業フォルダのパスは /path/to/app に置き換えています)。
/path/to/app/src/cases.tsx
11:7 error Missing an explicit type attribute for button react/button-has-type
44:7 error Missing an explicit type attribute for button react/button-has-type
45:7 error Missing an explicit type attribute for button react/button-has-type
53:10 error Missing an explicit type attribute for button react/button-has-type
92:7 error Missing an explicit type attribute for button react/button-has-type
93:7 error Missing an explicit type attribute for button react/button-has-type
105:7 error Missing an explicit type attribute for button react/button-has-type
11行目は NoType、44・45行目は送信したボタンを調べる例の「表示」と「ログイン」、92・93行目はフォームの外に置いたボタン、105行目は onClick で preventDefault を呼ぶ例です。53行目は、Button コンポーネントの中の <button className="btn" {...props} /> です。一方で、<Button onClick={...}>表示</Button> と使っている側の行はエラーになりませんでした。このルールが調べるのは button 要素そのものなので、自作のコンポーネントは、中の button で type を決めておく必要があります。SafeButton の type={type === 'submit' ? 'submit' : 'button'} は、エラーになりませんでした。





