Reactでフォーム送信ができない|preventDefaultの使い方

Reactでフォーム送信が動作しない問題とpreventDefaultの正しい使い方を詳しく解説。フォームハンドリングの基本からトラブルシューティングまで実践的に紹介

Learning Next 運営
52 分で読めます

あなたも、Reactでフォームを作った時にこんな経験はありませんか?

「送信ボタンを押してもページが更新されちゃう...」「フォームデータが送信されない...」「何かうまくいかない...」

フォームの実装って、React開発で避けて通れない重要な部分ですよね。 でも、初心者の方にとってはちょっと複雑で、特にpreventDefaultって何?という感じですよね。

でも大丈夫です!

この記事では、Reactでフォーム送信ができない原因と、preventDefaultの正しい使い方について詳しく解説します。 基本的な概念から実践的なトラブルシューティングまで、コード例と一緒に学んでいきましょう。

フォーム送信の基本って何?まずはここから理解しよう

まず、HTMLフォームとReactでのフォーム処理の違いを理解しましょう。 この違いがわかると、なぜpreventDefaultが必要なのかがスッキリしますよ。

普通のHTMLフォームはこんな動きをします

HTMLフォームは、デフォルトでサーバーにデータを送信してページを更新します。

<!-- 通常のHTMLフォーム -->
<form action="/submit" method="POST">
  <input type="text" name="username" />
  <input type="email" name="email" />
  <button type="submit">送信</button>
</form>

このHTMLフォームでは、こんな流れで動きます。

  1. 送信ボタンがクリックされる
  2. フォームデータが /submit にPOSTされる
  3. ページが新しいURLに移動する(ページ更新)

昔ながらのWebサイトなら、これで問題ありません。 でも、Reactでは少し違うんです。

Reactでのフォーム処理は何が違うの?

Reactでは、通常はページを更新せずにJavaScriptでフォームを処理します。

// ❌ 問題のあるReactフォーム
function ContactForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: ''
  });
  
  const handleSubmit = () => {
    // フォームデータの処理
    console.log('送信データ:', formData);
    // API呼び出しやバリデーション等
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={formData.username}
        onChange={(e) => setFormData({...formData, username: e.target.value})}
      />
      <input 
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({...formData, email: e.target.value})}
      />
      <button type="submit">送信</button>
    </form>
  );
}

でも、このコードには問題があります。

送信ボタンをクリックすると、handleSubmitが実行された後、HTMLフォームのデフォルト動作も実行されてしまうんです。 その結果、ページが更新されて、Reactの状態やコンソールログが失われてしまいます。

「あれ?何も起こらない...」となるのは、これが原因なんです。

preventDefaultって何?これがフォーム処理の鍵

preventDefaultは、ブラウザのデフォルト動作を阻止するためのメソッドです。

簡単に言うと、「ブラウザさん、いつものことはしないで!JavaScriptに任せて!」ということを伝える方法なんです。

eventオブジェクトを理解しよう

まず、イベントオブジェクトという便利なものを見てみましょう。

// イベントオブジェクトの確認
function ExampleForm() {
  const handleSubmit = (event) => {
    console.log('eventオブジェクト:', event);
    console.log('event.type:', event.type); // "submit"
    console.log('event.target:', event.target); // <form>要素
    console.log('event.preventDefault:', typeof event.preventDefault); // "function"
    
    // ここでpreventDefaultを呼ばないと、ページが更新される
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" />
      <button type="submit">送信</button>
    </form>
  );
}

handleSubmit関数の引数eventに、イベントに関する詳細な情報が入っています。 その中にpreventDefaultという関数があるんです。

preventDefaultの正しい使い方

では、正しい使い方を見てみましょう。

// ✅ 正しいフォーム処理
function ContactForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: ''
  });
  
  const handleSubmit = (event) => {
    // 重要:最初にpreventDefaultを呼ぶ
    event.preventDefault();
    
    console.log('送信データ:', formData);
    
    // ここでフォームデータの処理
    // API呼び出し、バリデーション等
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={formData.username}
        onChange={(e) => setFormData({...formData, username: e.target.value})}
        placeholder="ユーザー名"
      />
      <input 
        type="email"
        value={formData.email}
        onChange={(e) => setFormData({...formData, email: e.target.value})}
        placeholder="メールアドレス"
      />
      <button type="submit">送信</button>
    </form>
  );
}

event.preventDefault();を最初に書くのがポイントです! これで、ページが更新されることなく、Reactでフォームを処理できるようになります。

どんな時にpreventDefaultが必要?

フォーム送信以外にも、preventDefaultが必要な場面があります。

フォーム送信の場合

  • イベント: onSubmit
  • デフォルト動作: ページの更新・リロード
  • preventDefault: 必須

リンククリックの場合

  • イベント: onClick
  • デフォルト動作: リンク先への移動
  • preventDefault: SPA内で独自処理する場合

キーボード入力の場合

  • イベント: onKeyDown
  • デフォルト動作: 文字の入力、ショートカット実行
  • preventDefault: カスタムショートカットを実装する場合

Reactでは、特にフォーム送信で使うことが多いですね。

よくある間違いパターンを知って、同じ失敗を避けよう

フォーム処理でよく発生する間違いとその解決方法をご紹介します。 初心者の方がよくハマるポイントなので、ぜひチェックしてくださいね。

1. preventDefaultを呼び忘れている

これが一番多い間違いです。

// ❌ preventDefaultを呼んでいない
function LoginForm() {
  const [credentials, setCredentials] = useState({
    email: '',
    password: ''
  });
  
  const handleSubmit = (event) => {
    // preventDefault() を呼んでいない!
    
    console.log('ログイン試行:', credentials);
    // この後、ページが更新されてしまう
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="email"
        value={credentials.email}
        onChange={(e) => setCredentials({...credentials, email: e.target.value})}
      />
      <input 
        type="password"
        value={credentials.password}
        onChange={(e) => setCredentials({...credentials, password: e.target.value})}
      />
      <button type="submit">ログイン</button>
    </form>
  );
}

正しい書き方はこちらです。

// ✅ 正しい実装
function LoginForm() {
  const [credentials, setCredentials] = useState({
    email: '',
    password: ''
  });
  
  const handleSubmit = (event) => {
    event.preventDefault(); // これが重要!
    
    console.log('ログイン試行:', credentials);
    
    // ここでログイン処理
    authenticateUser(credentials);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="email"
        value={credentials.email}
        onChange={(e) => setCredentials({...credentials, email: e.target.value})}
      />
      <input 
        type="password"
        value={credentials.password}
        onChange={(e) => setCredentials({...credentials, password: e.target.value})}
      />
      <button type="submit">ログイン</button>
    </form>
  );
}

event.preventDefault(); この一行を忘れずに!

2. eventオブジェクトを受け取っていない

関数の引数でeventを受け取ることを忘れがちです。

// ❌ eventパラメータがない
function SearchForm() {
  const [query, setQuery] = useState('');
  
  const handleSubmit = () => { // eventパラメータがない
    // event.preventDefault(); // これはエラーになる
    console.log('検索:', query);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <button type="submit">検索</button>
    </form>
  );
}

正しくは、こうです。

// ✅ 正しい実装
function SearchForm() {
  const [query, setQuery] = useState('');
  
  const handleSubmit = (event) => { // eventパラメータを受け取る
    event.preventDefault();
    console.log('検索:', query);
    
    // 検索処理の実装
    performSearch(query);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="検索キーワード"
      />
      <button type="submit">検索</button>
    </form>
  );
}

関数の引数で(event)を忘れずに書きましょう。

3. ボタンのtypeを間違えている

ボタンのtype属性も重要なポイントです。

// ❌ ボタンのtype指定が間違っている
function CommentForm() {
  const [comment, setComment] = useState('');
  
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('コメント:', comment);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <textarea 
        value={comment}
        onChange={(e) => setComment(e.target.value)}
      />
      {/* type="button" だとフォーム送信されない */}
      <button type="button" onClick={handleSubmit}>送信</button>
    </form>
  );
}

正しい方法は2つあります。

方法1:type="submit"を使う

// ✅ 正しい実装(方法1)
function CommentForm() {
  const [comment, setComment] = useState('');
  
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('コメント:', comment);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <textarea 
        value={comment}
        onChange={(e) => setComment(e.target.value)}
      />
      {/* type="submit" でフォーム送信される */}
      <button type="submit">送信</button>
    </form>
  );
}

方法2:onClickハンドラを使う

// ✅ 正しい実装(方法2)
function CommentForm() {
  const [comment, setComment] = useState('');
  
  const handleClick = (event) => {
    event.preventDefault();
    console.log('コメント:', comment);
  };
  
  return (
    <form>
      <textarea 
        value={comment}
        onChange={(e) => setComment(e.target.value)}
      />
      {/* type="button" + onClickで明示的に処理 */}
      <button type="button" onClick={handleClick}>送信</button>
    </form>
  );
}

どちらでも動きますが、フォームの場合は方法1のtype="submit"がおすすめです。

4. 非同期処理での落とし穴

非同期処理(async/await)を使う場合の注意点もあります。

// ❌ 非同期処理で問題が起こる例
function RegistrationForm() {
  const [userData, setUserData] = useState({
    name: '',
    email: '',
    password: ''
  });
  
  const handleSubmit = async (event) => {
    // preventDefault を await の後に置くのは危険
    
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      });
      
      // ここでpreventDefaultを呼んでも遅い
      event.preventDefault(); // この時点では既にページが更新されている可能性
      
      const result = await response.json();
      console.log('登録成功:', result);
      
    } catch (error) {
      console.error('登録エラー:', error);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={userData.name}
        onChange={(e) => setUserData({...userData, name: e.target.value})}
      />
      <input 
        type="email"
        value={userData.email}
        onChange={(e) => setUserData({...userData, email: e.target.value})}
      />
      <input 
        type="password"
        value={userData.password}
        onChange={(e) => setUserData({...userData, password: e.target.value})}
      />
      <button type="submit">登録</button>
    </form>
  );
}

正しくは、最初にpreventDefaultを呼ぶことが重要です。

// ✅ 正しい実装
function RegistrationForm() {
  const [userData, setUserData] = useState({
    name: '',
    email: '',
    password: ''
  });
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleSubmit = async (event) => {
    // 最初にpreventDefaultを呼ぶ
    event.preventDefault();
    
    if (isSubmitting) return; // 重複送信防止
    
    try {
      setIsSubmitting(true);
      
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      });
      
      const result = await response.json();
      
      if (response.ok) {
        console.log('登録成功:', result);
        // 成功時の処理(リダイレクトなど)
      } else {
        throw new Error(result.message);
      }
      
    } catch (error) {
      console.error('登録エラー:', error);
      alert('登録に失敗しました: ' + error.message);
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={userData.name}
        onChange={(e) => setUserData({...userData, name: e.target.value})}
        placeholder="名前"
        required
      />
      <input 
        type="email"
        value={userData.email}
        onChange={(e) => setUserData({...userData, email: e.target.value})}
        placeholder="メールアドレス"
        required
      />
      <input 
        type="password"
        value={userData.password}
        onChange={(e) => setUserData({...userData, password: e.target.value})}
        placeholder="パスワード"
        required
      />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '登録中...' : '登録'}
      </button>
    </form>
  );
}

event.preventDefault();は、関数の一番最初に書くのがポイントです。 非同期処理の前に必ず呼びましょう!

実際のアプリで使える!実践的なフォーム実装例

実際のアプリケーションでよく使用されるフォームパターンをご紹介します。 ここまで理解できたら、より実践的なものにチャレンジしてみましょう。

バリデーション付きフォーム

実際のアプリでは、入力内容をチェックする機能が必要ですよね。

// バリデーション機能付きのフォーム
function ValidatedForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    confirmPassword: ''
  });
  
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  // バリデーション関数
  const validateForm = () => {
    const newErrors = {};
    
    // メールアドレスの検証
    if (!formData.email) {
      newErrors.email = 'メールアドレスは必須です';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = '有効なメールアドレスを入力してください';
    }
    
    // パスワードの検証
    if (!formData.password) {
      newErrors.password = 'パスワードは必須です';
    } else if (formData.password.length < 8) {
      newErrors.password = 'パスワードは8文字以上で入力してください';
    }
    
    // パスワード確認の検証
    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'パスワードが一致しません';
    }
    
    return newErrors;
  };
  
  const handleSubmit = async (event) => {
    event.preventDefault();
    
    // バリデーション実行
    const validationErrors = validateForm();
    setErrors(validationErrors);
    
    // エラーがある場合は送信しない
    if (Object.keys(validationErrors).length > 0) {
      return;
    }
    
    try {
      setIsSubmitting(true);
      
      // API呼び出し
      const response = await fetch('/api/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email: formData.email,
          password: formData.password
        })
      });
      
      if (response.ok) {
        alert('登録が完了しました');
        // フォームリセット
        setFormData({ email: '', password: '', confirmPassword: '' });
      } else {
        const error = await response.json();
        throw new Error(error.message);
      }
      
    } catch (error) {
      alert('エラー: ' + error.message);
    } finally {
      setIsSubmitting(false);
    }
  };
  
  const handleInputChange = (field) => (event) => {
    setFormData({
      ...formData,
      [field]: event.target.value
    });
    
    // エラーをクリア
    if (errors[field]) {
      setErrors({
        ...errors,
        [field]: ''
      });
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input 
          id="email"
          type="email"
          value={formData.email}
          onChange={handleInputChange('email')}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && (
          <span className="error-message">{errors.email}</span>
        )}
      </div>
      
      <div>
        <label htmlFor="password">パスワード</label>
        <input 
          id="password"
          type="password"
          value={formData.password}
          onChange={handleInputChange('password')}
          className={errors.password ? 'error' : ''}
        />
        {errors.password && (
          <span className="error-message">{errors.password}</span>
        )}
      </div>
      
      <div>
        <label htmlFor="confirmPassword">パスワード確認</label>
        <input 
          id="confirmPassword"
          type="password"
          value={formData.confirmPassword}
          onChange={handleInputChange('confirmPassword')}
          className={errors.confirmPassword ? 'error' : ''}
        />
        {errors.confirmPassword && (
          <span className="error-message">{errors.confirmPassword}</span>
        )}
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '送信中...' : '登録'}
      </button>
    </form>
  );
}

長いコードですが、ポイントを見ていきましょう。

バリデーション関数では、入力内容をチェックしてエラーメッセージを返しています。 メールアドレスの形式チェックや、パスワードの長さチェックを行っています。

handleSubmit関数では、まずevent.preventDefault()を呼んでから、バリデーションを実行しています。 エラーがあれば送信せず、エラーがなければAPI呼び出しを行います。

エラー表示では、各入力フィールドの下にエラーメッセージを表示しています。 ユーザーが入力を修正すると、エラーが自動的にクリアされます。

複数ステップのフォーム

大きなフォームは、複数のステップに分けると使いやすくなります。

// 複数ステップに分かれたフォーム
function MultiStepForm() {
  const [currentStep, setCurrentStep] = useState(1);
  const [formData, setFormData] = useState({
    // ステップ1
    personalInfo: {
      firstName: '',
      lastName: '',
      email: ''
    },
    // ステップ2
    address: {
      street: '',
      city: '',
      zipCode: ''
    },
    // ステップ3
    preferences: {
      newsletter: false,
      notifications: true
    }
  });
  
  const handleSubmit = async (event) => {
    event.preventDefault();
    
    if (currentStep < 3) {
      // 次のステップに進む
      setCurrentStep(currentStep + 1);
    } else {
      // 最終送信
      try {
        const response = await fetch('/api/complete-registration', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(formData)
        });
        
        if (response.ok) {
          alert('登録が完了しました!');
        }
      } catch (error) {
        alert('エラーが発生しました');
      }
    }
  };
  
  const handlePrevious = (event) => {
    event.preventDefault(); // ボタンのデフォルト動作を防ぐ
    if (currentStep > 1) {
      setCurrentStep(currentStep - 1);
    }
  };
  
  const updateFormData = (section, field, value) => {
    setFormData({
      ...formData,
      [section]: {
        ...formData[section],
        [field]: value
      }
    });
  };
  
  const renderStep = () => {
    switch (currentStep) {
      case 1:
        return (
          <div>
            <h2>個人情報</h2>
            <input 
              type="text"
              placeholder="姓"
              value={formData.personalInfo.firstName}
              onChange={(e) => updateFormData('personalInfo', 'firstName', e.target.value)}
            />
            <input 
              type="text"
              placeholder="名"
              value={formData.personalInfo.lastName}
              onChange={(e) => updateFormData('personalInfo', 'lastName', e.target.value)}
            />
            <input 
              type="email"
              placeholder="メールアドレス"
              value={formData.personalInfo.email}
              onChange={(e) => updateFormData('personalInfo', 'email', e.target.value)}
            />
          </div>
        );
      
      case 2:
        return (
          <div>
            <h2>住所情報</h2>
            <input 
              type="text"
              placeholder="住所"
              value={formData.address.street}
              onChange={(e) => updateFormData('address', 'street', e.target.value)}
            />
            <input 
              type="text"
              placeholder="市区町村"
              value={formData.address.city}
              onChange={(e) => updateFormData('address', 'city', e.target.value)}
            />
            <input 
              type="text"
              placeholder="郵便番号"
              value={formData.address.zipCode}
              onChange={(e) => updateFormData('address', 'zipCode', e.target.value)}
            />
          </div>
        );
      
      case 3:
        return (
          <div>
            <h2>設定</h2>
            <label>
              <input 
                type="checkbox"
                checked={formData.preferences.newsletter}
                onChange={(e) => updateFormData('preferences', 'newsletter', e.target.checked)}
              />
              ニュースレターを受け取る
            </label>
            <label>
              <input 
                type="checkbox"
                checked={formData.preferences.notifications}
                onChange={(e) => updateFormData('preferences', 'notifications', e.target.checked)}
              />
              通知を受け取る
            </label>
          </div>
        );
      
      default:
        return null;
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div className="progress">
        ステップ {currentStep} / 3
      </div>
      
      {renderStep()}
      
      <div className="form-actions">
        {currentStep > 1 && (
          <button type="button" onClick={handlePrevious}>
            戻る
          </button>
        )}
        <button type="submit">
          {currentStep < 3 ? '次へ' : '送信'}
        </button>
      </div>
    </form>
  );
}

この例では、現在のステップを管理して、段階的にフォームを進めています。

handleSubmit関数では、最後のステップでない場合は次のステップに進み、最後のステップでは実際にデータを送信しています。

handlePrevious関数では、event.preventDefault()を呼んで、前のステップに戻っています。

updateFormData関数では、ネストした状態を更新しています。 これで、各ステップのデータを適切に管理できます。

ファイルアップロード付きフォーム

ファイルアップロード機能も実装してみましょう。

// ファイルアップロード機能付きフォーム
function FileUploadForm() {
  const [formData, setFormData] = useState({
    title: '',
    description: '',
    file: null
  });
  
  const [uploadProgress, setUploadProgress] = useState(0);
  const [isUploading, setIsUploading] = useState(false);
  
  const handleFileChange = (event) => {
    const file = event.target.files[0];
    setFormData({
      ...formData,
      file: file
    });
  };
  
  const handleSubmit = async (event) => {
    event.preventDefault();
    
    if (!formData.file) {
      alert('ファイルを選択してください');
      return;
    }
    
    try {
      setIsUploading(true);
      setUploadProgress(0);
      
      // FormData を使用してファイルを送信
      const formDataToSend = new FormData();
      formDataToSend.append('title', formData.title);
      formDataToSend.append('description', formData.description);
      formDataToSend.append('file', formData.file);
      
      // XMLHttpRequest を使用してアップロード進捗を追跡
      const xhr = new XMLHttpRequest();
      
      xhr.upload.addEventListener('progress', (event) => {
        if (event.lengthComputable) {
          const progress = Math.round((event.loaded / event.total) * 100);
          setUploadProgress(progress);
        }
      });
      
      xhr.addEventListener('load', () => {
        if (xhr.status === 200) {
          alert('アップロードが完了しました!');
          // フォームリセット
          setFormData({ title: '', description: '', file: null });
          setUploadProgress(0);
        } else {
          throw new Error('アップロードに失敗しました');
        }
      });
      
      xhr.addEventListener('error', () => {
        throw new Error('ネットワークエラーが発生しました');
      });
      
      xhr.open('POST', '/api/upload');
      xhr.send(formDataToSend);
      
    } catch (error) {
      alert('エラー: ' + error.message);
    } finally {
      setIsUploading(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="title">タイトル</label>
        <input 
          id="title"
          type="text"
          value={formData.title}
          onChange={(e) => setFormData({...formData, title: e.target.value})}
          required
        />
      </div>
      
      <div>
        <label htmlFor="description">説明</label>
        <textarea 
          id="description"
          value={formData.description}
          onChange={(e) => setFormData({...formData, description: e.target.value})}
        />
      </div>
      
      <div>
        <label htmlFor="file">ファイル</label>
        <input 
          id="file"
          type="file"
          onChange={handleFileChange}
          accept="image/*,application/pdf"
          required
        />
        {formData.file && (
          <p>選択されたファイル: {formData.file.name}</p>
        )}
      </div>
      
      {isUploading && (
        <div className="upload-progress">
          <div className="progress-bar">
            <div 
              className="progress-fill"
              style={{ width: `${uploadProgress}%` }}
            />
          </div>
          <span>{uploadProgress}%</span>
        </div>
      )}
      
      <button type="submit" disabled={isUploading}>
        {isUploading ? 'アップロード中...' : 'アップロード'}
      </button>
    </form>
  );
}

ファイルアップロードでは、FormDataという特殊なオブジェクトを使います。

handleFileChange関数では、選択されたファイルを状態に保存しています。

handleSubmit関数では、event.preventDefault()を呼んでから、FormDataを作成してサーバーに送信しています。

アップロード進捗も表示できるように、XMLHttpRequestを使って進捗を追跡しています。 ユーザーにとって親切ですよね。

困った時のデバッグ方法を覚えよう

フォーム送信の問題を効率的にデバッグする方法をご紹介します。 「なんかうまくいかない...」という時に役立ちますよ。

デバッグの基本ステップ

まず、問題の原因を特定するために、コンソールログを使って状況を確認しましょう。

// デバッグ用のコンソールログ
function DebugForm() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  
  const handleSubmit = (event) => {
    console.log('=== フォーム送信デバッグ ===');
    console.log('1. handleSubmit が呼ばれました');
    console.log('2. event オブジェクト:', event);
    console.log('3. event.type:', event.type);
    console.log('4. event.target:', event.target);
    
    // preventDefault を呼ぶ前の状態を確認
    console.log('5. preventDefault を呼ぶ前');
    
    event.preventDefault();
    
    console.log('6. preventDefault を呼んだ後');
    console.log('7. フォームデータ:', formData);
    
    // この後に実際の処理
    console.log('8. 実際の処理を開始');
  };
  
  const handleInputChange = (field) => (event) => {
    console.log(`${field} が変更されました:`, event.target.value);
    setFormData({
      ...formData,
      [field]: event.target.value
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        value={formData.name}
        onChange={handleInputChange('name')}
        placeholder="名前"
      />
      <input 
        type="email"
        value={formData.email}
        onChange={handleInputChange('email')}
        placeholder="メール"
      />
      <button type="submit">送信</button>
    </form>
  );
}

このように、各ステップでconsole.logを入れることで、どこで問題が起きているかがわかります。

ブラウザの開発者ツール(F12キー)のコンソールタブを見ながら、フォームを送信してみてください。

よくある問題の診断方法

ページが更新される場合

  • 原因: preventDefault()を呼んでいない、eventオブジェクトを受け取っていない
  • 確認方法: console.logでイベントハンドラが呼ばれているか確認、eventオブジェクトの存在確認

フォームデータが送信されない場合

  • 原因: inputのvalueとonChangeが正しく設定されていない、状態更新のタイミングの問題
  • 確認方法: React Developer Toolsで状態を確認、Networkタブで API呼び出しを確認

バリデーションが動作しない場合

  • 原因: バリデーション関数の実装ミス、エラー状態の管理が不適切
  • 確認方法: バリデーション関数を単体でテスト、エラー状態をコンソールで確認

実際の診断用コンポーネント

本格的なデバッグ用のコンポーネントも作ってみました。

// 実際の診断用コンポーネント
function DiagnosticForm() {
  const [formData, setFormData] = useState({ email: '', password: '' });
  const [diagnostics, setDiagnostics] = useState([]);
  
  const addDiagnostic = (message) => {
    const timestamp = new Date().toLocaleTimeString();
    setDiagnostics(prev => [
      ...prev,
      { timestamp, message }
    ]);
  };
  
  const handleSubmit = (event) => {
    addDiagnostic('handleSubmit が呼ばれました');
    
    if (!event) {
      addDiagnostic('❌ event オブジェクトが undefined です');
      return;
    } else {
      addDiagnostic('✅ event オブジェクトを受け取りました');
    }
    
    if (typeof event.preventDefault !== 'function') {
      addDiagnostic('❌ preventDefault が利用できません');
      return;
    } else {
      addDiagnostic('✅ preventDefault を実行します');
      event.preventDefault();
      addDiagnostic('✅ preventDefault を実行しました');
    }
    
    addDiagnostic(`📋 フォームデータ: ${JSON.stringify(formData)}`);
    
    // バリデーション確認
    if (!formData.email || !formData.password) {
      addDiagnostic('❌ 必須項目が入力されていません');
      return;
    } else {
      addDiagnostic('✅ バリデーション通過');
    }
    
    addDiagnostic('🚀 送信処理を開始します');
  };
  
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input 
          type="email"
          value={formData.email}
          onChange={(e) => setFormData({...formData, email: e.target.value})}
          placeholder="メールアドレス"
        />
        <input 
          type="password"
          value={formData.password}
          onChange={(e) => setFormData({...formData, password: e.target.value})}
          placeholder="パスワード"
        />
        <button type="submit">送信</button>
      </form>
      
      <div style={{ marginTop: '20px', border: '1px solid #ccc', padding: '10px' }}>
        <h3>診断ログ</h3>
        <button onClick={() => setDiagnostics([])}>ログクリア</button>
        <div style={{ height: '200px', overflow: 'auto', fontSize: '12px' }}>
          {diagnostics.map((log, index) => (
            <div key={index}>
              <strong>{log.timestamp}</strong>: {log.message}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

この診断用コンポーネントでは、フォーム送信の各ステップを詳細にログに記録しています。

addDiagnostic関数で、タイムスタンプ付きのメッセージを記録しています。

handleSubmit関数では、eventオブジェクトの確認、preventDefaultの実行、フォームデータの確認、バリデーションの確認を順番に行い、それぞれの結果をログに出力しています。

問題が起きた時は、このログを見ることで、どこで問題が発生しているかがすぐにわかります。

まとめ:Reactのフォーム処理をマスターしよう!

お疲れ様でした! Reactでのフォーム送信とpreventDefaultの使い方について、詳しく学んできました。

重要なポイントをもう一度おさらいしましょう

  • event.preventDefault()を忘れずに:フォーム送信時は必ず最初に呼ぶ
  • eventオブジェクトを受け取る:関数の引数で(event)を忘れずに
  • 非同期処理でも最初にpreventDefault:awaitの前に必ず呼ぶ
  • ボタンのtype属性:submitかbuttonか、用途に応じて使い分ける

実践的なスキルも身につきました

  • バリデーション機能付きフォーム
  • 複数ステップのフォーム
  • ファイルアップロード機能
  • デバッグとトラブルシューティング

preventDefaultは、Reactでフォーム処理をする上で絶対に欠かせない基本中の基本です。 最初は「なんで必要なの?」と思うかもしれませんが、ブラウザのデフォルト動作とReactの仕組みを理解すれば、必要性がよくわかりますよね。

問題が発生した時は、段階的なデバッグを行うことで、効率的に原因を特定できます。 コンソールログをうまく活用して、一歩ずつ問題を解決していきましょう。

ぜひ今回の内容を参考に、正しいフォーム処理を実装して、使いやすいWebアプリケーションを作ってみてくださいね!

関連記事