投稿日:2026/8/8
更新日:2026/8/8

preventDefault() の使いどころ(stopPropagation() との違い)フォームの onSubmit でとりあえず書いている event.preventDefault() が、結局何を止めているのかを整理する。
よく出てくる場面のカタログと、混同しがちな stopPropagation() との違い、効かないケースまでまとめた。
preventDefault() は ブラウザが元々やろうとする動作(デフォルト動作)を止めるだけ。stopPropagation() で、両者は別物。cancelable: false のイベントや passive リスナーでは 何も起きない(scroll や React の onWheel など)。ブラウザは、JavaScript が何も書かれていなくても要素ごとに決まった動作をする。
| イベント | ブラウザのデフォルト動作 |
|---|---|
submit(form) |
フォームを送信してページを遷移/リロードする |
click(a タグ) |
href 先へ遷移する |
click(checkbox) |
チェック状態を切り替える |
keydown(Cmd/Ctrl + S) |
「ページを保存」ダイアログを開く |
keydown(Space / 矢印キー) |
ページをスクロールする |
contextmenu |
ブラウザ標準の右クリックメニューを出す |
drop |
ドロップされたファイルをそのタブで開く |
preventDefault() は、この「決まった動作」だけをキャンセルする。
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// ページ遷移させずに API 送信する
saveForm();
};
return <form onSubmit={handleSubmit}>{/* ... */}</form>;
preventDefault() が無いとどうなるかを時系列で見ると分かりやすい。
sequenceDiagram
participant U as ユーザー
participant B as ブラウザ
participant H as onSubmit ハンドラ
participant S as APIサーバー
U->>B: 送信ボタンをクリック
B->>H: submit イベント
H->>H: preventDefault()
H->>S: fetch("/api/orders")
S-->>H: 200 OK
H->>B: 画面を更新(遷移なし)
Note over B: preventDefault() が無いと<br/>ここでページがリロードされ<br/>fetch が中断される
フォームでは
preventDefault()を忘れると「一瞬 API が飛んだように見えるがページがリロードされる」という挙動になり、原因が分かりにくい。
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
if (!isDirty) return; // 未保存の変更が無ければ、そのまま普通に遷移させる
event.preventDefault(); // 遷移を止める
setShowConfirmDialog(true);
};
return <a href="/orders" onClick={handleClick}>注文一覧へ</a>;
条件付きで preventDefault() を呼ぶのがポイント。無条件に止めてしまうと、Cmd + クリックの別タブ表示なども潰れる。
グローバルなキーイベントなので window に直接張る。
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key === 's') {
event.preventDefault(); // ブラウザの「ページを保存」を止める
save();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [save]);
ゲームやエディタなど、キー入力を自前で扱う画面で必要になる。
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.code === 'Space' || event.key.startsWith('Arrow')) {
event.preventDefault(); // ページのスクロールを止める
}
moveCursor(event.key);
};
ここは「止めないと動かない」珍しいパターン。dragover のデフォルト動作を止めないと drop が発火しない。
<div
onDragOver={(event) => event.preventDefault()} // これが無いと onDrop が呼ばれない
onDrop={(event) => {
event.preventDefault(); // ブラウザがファイルをそのタブで開くのを止める
const files = Array.from(event.dataTransfer.files);
upload(files);
}}
>
ここにファイルをドロップ
</div>
<div
onContextMenu={(event) => {
event.preventDefault(); // ブラウザ標準メニューを止める
openMenu(event.clientX, event.clientY);
}}
>
preventDefault() と stopPropagation() は別物一番の勘所。両者は イベントの流れの違うところを切っている。
flowchart TD
A["イベント発生"] --> B["キャプチャ<br/>window → 親 → ターゲット"]
B --> C["ターゲットのハンドラ"]
C --> D["バブリング<br/>親 → window"]
D --> E["ブラウザのデフォルト動作<br/>(遷移・リロードなど)"]
P["preventDefault()"] -. "止める" .-> E
S["stopPropagation()"] -. "止める" .-> D
preventDefault() |
stopPropagation() |
stopImmediatePropagation() |
|
|---|---|---|---|
| 止めるもの | ブラウザのデフォルト動作 | 親要素へのイベント伝播 | 伝播 + 同じ要素の残りのハンドラ |
| 親のハンドラは走る? | 走る | 走らない | 走らない |
| デフォルト動作は起きる? | 起きない | 起きる | 起きる |
つまり、
stopPropagation()preventDefault()const handleClick = (event: React.MouseEvent) => {
event.preventDefault(); // 遷移を止める
event.stopPropagation(); // 親のクリックハンドラを走らせない
};
React のハンドラで
return falseと書いても何も起きない。jQuery やインラインonclickでは「preventDefault+stopPropagation」の意味だったが、React では単なる戻り値として捨てられる。
addEventListener と React の onXxx の使い分け実務感でいうと、境界はだいたいこう分かれる。
| 使う場面 | |
|---|---|
addEventListener |
React の外、window / document に張るグローバルなイベント(ショートカット、リサイズ、beforeunload) |
React の onXxx |
フォーム、リンク、要素内のドラッグ&ドロップ、右クリックなど特定の要素に紐づくイベント |
React の onXxx で足りるならそちらを使う。addEventListener はクリーンアップ(removeEventListener)を自分で書く必要があり、書き忘れるとリスナーが積み上がる。
cancelable: false のイベントには効かないscroll のように「もう起きてしまった結果」を通知するイベントは、そもそもキャンセルできない。
element.addEventListener('scroll', (event) => {
console.log(event.cancelable); // false
event.preventDefault(); // 何も起きない
});
スクロールを止めたいなら、原因側の wheel / touchmove を止めるか、CSS の overflow: hidden を使う。
止まったかどうかは defaultPrevented で確認できる。
event.preventDefault();
console.log(event.defaultPrevented); // true なら実際に止まっている
{ passive: true } は「このハンドラは preventDefault() しません」という宣言で、スクロール性能のために使われる。宣言に反して呼ぶと無視され、コンソールに警告が出る。
さらに厄介なのが、touchstart / touchmove / wheel を window / document / body に張ると ブラウザが既定で passive 扱いにすること。止めたいなら明示する。
window.addEventListener('touchmove', handleTouchMove, { passive: false }); // ✅ 効く
onWheel / onTouchMove で preventDefault() が効かないReact 17 以降、onWheel / onTouchStart / onTouchMove は passive リスナーとして登録される。React のハンドラ内で preventDefault() を書いても効かない。
ref から native リスナーを張り直すのが定石。
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const handleWheel = (event: WheelEvent) => {
event.preventDefault(); // ✅ passive: false なので効く
zoom(event.deltaY);
};
el.addEventListener('wheel', handleWheel, { passive: false });
return () => el.removeEventListener('wheel', handleWheel);
}, []);
return <div ref={ref}>{/* ... */}</div>;
<form> の中の <button> は既定で type="submit"「ただの操作ボタンのつもりが、押すとページがリロードされる」の原因はほぼこれ。
<button type="button" onClick={addRow}>行を追加</button> {/* ✅ type を明示する */}
なお、送信自体は type="button" で回避せず、onSubmit + preventDefault() を基本にする。Enter キーでの送信やブラウザ標準のバリデーションが効かなくなるため。