フロントエンドの失敗例・バグ解説シリーズ【保存版】

「なぜか動かない」「思った挙動と違う」
フロントエンド開発において、こうした“バグ”や“失敗例”は日常茶飯事です。

今回は、初心者がよくハマるけれど意外と体系的にまとめられていない代表的なバグ・誤解について、

  • setTimeout がループして止まらない
  • イベントが二重に発火する
  • CORSエラーの種類と読み解き方
  • fetch がPOSTでも body を送らない

という4つのテーマで、なぜ起きるのか・どう直すのか・どう防ぐのか を徹底的に解説します。


✅ 1. setTimeout がループして止まらない理由

🔥 よくあるコード:

function repeatLog() {
  setTimeout(() => {
    console.log('Hello');
    repeatLog(); // 自分自身を再呼び出し
  }, 1000);
}

repeatLog();

このコード、一見タイマーで1秒ごとに実行される「正しい」使い方に見えますが、ループが止まらなくなります。

⚠️ 問題点

setTimeout の中で 再帰的に自分自身を呼び出している ため、止める手段がありません。setTimeoutsetInterval と違って返り値の timeoutId を元に clearTimeout する必要がありますが、これではキャンセルできない構造です。

✅ 正しい書き方(停止できる構造に):

let timerId;

function repeatLog() {
  timerId = setTimeout(() => {
    console.log('Hello');
    repeatLog();
  }, 1000);
}

function stop() {
  clearTimeout(timerId);
}

repeatLog();
// stop() を呼べば止まる

💡 ワンポイント

  • setTimeout 再帰は「細かい間隔で制御したい」用途には便利(setInterval より柔軟)
  • ただし 再帰構造では常に停止手段を設けること
  • React では useEffect 内で setTimeout を使う場合、クリーンアップで clearTimeout するのが基本

✅ 2. イベントが二重に発火する典型的パターン

😱 よくある現象:

  • ボタンをクリックしたら2回実行された
  • フォームの submit が2回発火してバグった

🧨 原因パターン1:イベントの重複登録

document.getElementById('btn').addEventListener('click', handleClick);

function handleClick() {
  console.log('Clicked!');
}

これ自体は問題ありませんが、コンポーネントの再描画や、再レンダリング時に何度も addEventListener してしまうと、重複登録 になります。

🧹 対策

  • useEffect のクリーンアップで removeEventListener を忘れずに:
useEffect(() => {
  const btn = document.getElementById('btn');
  btn?.addEventListener('click', handleClick);

  return () => {
    btn?.removeEventListener('click', handleClick);
  };
}, []);

🧨 原因パターン2:フォーム submit に click イベントが重なっている

<form onSubmit={handleSubmit}>
  <button onClick={handleClick}>送信</button>
</form>

この場合、onClickonSubmit の両方が発火します。
かつ handleSubmite.preventDefault() をしていないと、ページがリロードされてバグになります。

✅ 対策:

  • どちらか1つに絞る or preventDefault() を忘れずに。
  • 特に React Hook Form を使う場合は、onSubmitだけで完結させること。

✅ 3. CORS エラーの種類と「読める」ようになる解説

CORS(クロスオリジンリソース共有)エラーは、内容が英語で長文なので一見わかりづらく、初心者が躓きやすいポイントです。

🎯 典型的なエラー文①:

Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

📌 これは何が起きている?

  • ブラウザが「他のドメインにリクエストを送ったけど、許可されてないよ」と拒否
  • サーバー側の Access-Control-Allow-Origin ヘッダーがない

✅ 対応策:

  • サーバー側に以下を設定:
Access-Control-Allow-Origin: *

もしくは特定のオリジン:

Access-Control-Allow-Origin: http://localhost:3000

フロント側では絶対に解決できません。CORSはサーバーの設定が必要です。


🎯 典型的なエラー文②:

Response to preflight request doesn't pass access control check:
It does not have HTTP ok status.

これは、OPTIONS リクエスト(プリフライト)に対してサーバーが拒否しているというエラーです。

✅ よくあるトリガー:

fetch('/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ msg: 'hello' })
});

このようにカスタムヘッダーがあると、プリフライト(事前確認)として OPTIONS リクエストが送られます
サーバーがそれに対応していないとエラーになります。

🛠 サーバー側(Express例):

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

✅ 4. fetch が POST でも body を送らない原因

意外と多いのが、「POSTリクエストを送ってるのに、サーバーで req.body が空」問題。

🔍 原因パターン1:Content-Type が指定されていない

fetch('/api/send', {
  method: 'POST',
  body: JSON.stringify({ msg: 'hello' })
});

このように書くと、ブラウザが Content-Type を設定してくれないことがあり、サーバーがJSONと認識せずパースできません。

✅ 対策:

fetch('/api/send', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ msg: 'hello' })
});

🔍 原因パターン2:Next.js API Routes 側で req.body を使う前提であるが、ミドルウェアが未設定

Next.js(App Router)では、デフォルトで Request オブジェクトが ReadableStream のままなので、bodyの取得には await req.json() が必要です。

export async function POST(req: Request) {
  const body = await req.json();
  console.log(body); // ここで初めて取得できる
}

🧠 まとめ:フロントエンドの「バグ原因」を自分で読めるようになる

症状原因解決策
setTimeoutが止まらない再帰+clearTimeoutなしタイマーID管理&停止関数用意
イベントが2回発火イベント重複登録・submitとclick重複useEffectのクリーンアップ or 絞り込み
CORSエラーサーバーがCORSヘッダー未設定サーバー側にヘッダー追加&OPTIONS対応
fetchでbodyが送れないContent-Type未設定 or await req.json()忘れfetch側とAPI側両方チェック

✍️ おわりに

今回紹介した4つのバグは、ReactやNext.jsでも日常的に起きる問題です。
初心者が「動かない…」と悩んだ時に、「なぜそうなるのか」を理解できれば、トラブルシューティング能力が一気に伸びます。

ぜひこの「バグ読解力」を武器に、よりスムーズなフロントエンド開発を目指していきましょう!

おすすめの記事