ReactでonChangeが動かない|入力フォームの基本

ReactでonChangeが動かない原因と解決方法を詳しく解説。入力フォームの基本的な実装から応用まで、実際のコード例で説明します。

Learning Next 運営
20 分で読めます

みなさん、ReactでonChangeが動かなくて困ったことはありませんか?

「入力しても画面が更新されない」「値が取得できない」と悩んだ経験はありませんか?

この記事では、ReactでonChangeが動かない原因と解決方法を詳しく解説します。 難しそうに見えますが、実は基本を押さえれば簡単に解決できるんです。

一緒にonChangeの仕組みをマスターして、スムーズに動くフォームを作りましょう!

onChangeが動かない主な原因

stateの更新方法が間違っている

最も多い原因は、stateの更新方法が間違っていることです。

import React, { useState } from 'react';

// ❌ 間違った例
function BrokenForm() {
    const [inputValue, setInputValue] = useState('');
    
    const handleChange = (e) => {
        // 間違い:stateを直接変更しようとしている
        inputValue = e.target.value; // これは動かない
    };
    
    return (
        <div>
            <input 
                type="text" 
                value={inputValue}
                onChange={handleChange}
            />
            <p>入力値: {inputValue}</p>
        </div>
    );
}

上のコードでは、inputValue = e.target.valueでstateを直接変更しようとしています。 これではReactが変更を検知できないので、画面が更新されません。

// ✅ 正しい例
function CorrectForm() {
    const [inputValue, setInputValue] = useState('');
    
    const handleChange = (e) => {
        // 正しい:setState関数を使用
        setInputValue(e.target.value);
    };
    
    return (
        <div>
            <input 
                type="text" 
                value={inputValue}
                onChange={handleChange}
            />
            <p>入力値: {inputValue}</p>
        </div>
    );
}

setInputValueを使うことで、Reactが変更を検知して画面を更新します。

イベントハンドラーの書き方が間違っている

onChangeの書き方にも注意が必要です。

// ❌ 間違ったイベントハンドラーの書き方
function WrongEventHandler() {
    const [value, setValue] = useState('');
    
    return (
        <input 
            type="text"
            value={value}
            // 間違い:関数を即座に実行している
            onChange={setValue(e.target.value)}
        />
    );
}

onChange={setValue(e.target.value)}と書くと、関数が即座に実行されてしまいます。

// ✅ 正しいイベントハンドラーの書き方
function CorrectEventHandler() {
    const [value, setValue] = useState('');
    
    return (
        <input 
            type="text"
            value={value}
            // 正しい:関数を渡している
            onChange={(e) => setValue(e.target.value)}
        />
    );
}

onChange={(e) => setValue(e.target.value)}のように、アロー関数で関数を渡します。

イベントオブジェクトを受け取っていない

イベントハンドラーでイベントオブジェクトを受け取り忘れることも多いです。

// ❌ 間違い:イベントオブジェクトを受け取っていない
function EventError() {
    const [value, setValue] = useState('');
    
    const handleChange = () => {
        // 間違い:イベントオブジェクトがない
        setValue(value); // 古い値を設定してしまう
    };
    
    return (
        <input 
            type="text"
            value={value}
            onChange={handleChange}
        />
    );
}

イベントオブジェクト(e)を受け取らないと、入力された値を取得できません。

// ✅ 正しい:イベントオブジェクトを受け取る
function EventCorrect() {
    const [value, setValue] = useState('');
    
    const handleChange = (e) => {
        // 正しい:イベントオブジェクトから値を取得
        setValue(e.target.value);
    };
    
    return (
        <input 
            type="text"
            value={value}
            onChange={handleChange}
        />
    );
}

handleChange = (e) => {}のように、必ずイベントオブジェクトを受け取りましょう。

制御されたコンポーネントの基本

Reactでフォームを扱う際は、制御されたコンポーネントの概念を理解することが重要です。

制御されたコンポーネントとは

制御されたコンポーネントとは、Reactのstateによって値が制御されるコンポーネントのことです。

function ControlledInput() {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    
    return (
        <form>
            <div>
                <label>名前:</label>
                <input 
                    type="text"
                    value={name}  // stateで値を制御
                    onChange={(e) => setName(e.target.value)}
                />
            </div>
            <div>
                <label>メール:</label>
                <input 
                    type="email"
                    value={email}  // stateで値を制御
                    onChange={(e) => setEmail(e.target.value)}
                />
            </div>
            <p>名前: {name}</p>
            <p>メール: {email}</p>
        </form>
    );
}

valueプロパティにstateを指定することで、Reactがinputの値を管理します。 onChangeでstateを更新することで、入力に応じて画面が更新されます。

制御されていないコンポーネント

制御されていないコンポーネントでは、DOMが値を管理します。

function UncontrolledInput() {
    const inputRef = useRef(null);
    
    const handleSubmit = () => {
        // DOMから直接値を取得
        console.log(inputRef.current.value);
    };
    
    return (
        <div>
            <input 
                type="text"
                ref={inputRef}
                // valueプロパティを設定しない
            />
            <button onClick={handleSubmit}>送信</button>
        </div>
    );
}

制御されていないコンポーネントでは、refを使ってDOMから直接値を取得します。

複数の入力フィールドを効率的に管理

複数の入力フィールドがある場合は、オブジェクトでstateを管理すると便利です。

非効率な方法

// ❌ 非効率な方法:各フィールドに個別のstate
function MultipleFieldsBad() {
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [phone, setPhone] = useState('');
    
    return (
        <form>
            <input 
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
            />
            <input 
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
            />
            <input 
                type="tel"
                value={phone}
                onChange={(e) => setPhone(e.target.value)}
            />
        </form>
    );
}

フィールドが多くなると、stateとイベントハンドラーが大量に必要になります。

効率的な方法

// ✅ 効率的な方法:オブジェクトでstateを管理
function MultipleFieldsGood() {
    const [formData, setFormData] = useState({
        name: '',
        email: '',
        phone: ''
    });
    
    const handleChange = (e) => {
        const { name, value } = e.target;
        setFormData(prev => ({
            ...prev,
            [name]: value
        }));
    };
    
    return (
        <form>
            <input 
                type="text"
                name="name"
                value={formData.name}
                onChange={handleChange}
            />
            <input 
                type="email"
                name="email"
                value={formData.email}
                onChange={handleChange}
            />
            <input 
                type="tel"
                name="phone"
                value={formData.phone}
                onChange={handleChange}
            />
        </form>
    );
}

1つのオブジェクトでstateを管理し、1つのイベントハンドラーで全てのフィールドに対応します。

const { name, value } = e.targetで入力フィールドの名前と値を取得。 [name]: valueでオブジェクトの該当プロパティを更新します。

実践的なフォームを作ってみよう

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

function LoginForm() {
    const [credentials, setCredentials] = useState({
        username: '',
        password: ''
    });
    const [errors, setErrors] = useState({});
    
    const handleChange = (e) => {
        const { name, value } = e.target;
        setCredentials(prev => ({
            ...prev,
            [name]: value
        }));
        
        // エラーをクリア
        if (errors[name]) {
            setErrors(prev => ({
                ...prev,
                [name]: ''
            }));
        }
    };
    
    const handleSubmit = (e) => {
        e.preventDefault();
        
        // バリデーション
        const newErrors = {};
        if (!credentials.username) {
            newErrors.username = 'ユーザー名を入力してください';
        }
        if (!credentials.password) {
            newErrors.password = 'パスワードを入力してください';
        }
        
        if (Object.keys(newErrors).length > 0) {
            setErrors(newErrors);
            return;
        }
        
        // ログイン処理
        console.log('ログイン情報:', credentials);
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>ユーザー名:</label>
                <input 
                    type="text"
                    name="username"
                    value={credentials.username}
                    onChange={handleChange}
                />
                {errors.username && <span className="error">{errors.username}</span>}
            </div>
            <div>
                <label>パスワード:</label>
                <input 
                    type="password"
                    name="password"
                    value={credentials.password}
                    onChange={handleChange}
                />
                {errors.password && <span className="error">{errors.password}</span>}
            </div>
            <button type="submit">ログイン</button>
        </form>
    );
}

このフォームでは、入力値の管理とバリデーションを同時に行っています。

handleChangeで入力値を更新し、同時にエラーをクリアします。 handleSubmitでバリデーションを実行し、エラーがあれば表示します。

動的なフォーム項目

function DynamicForm() {
    const [items, setItems] = useState([{ id: 1, name: '', price: '' }]);
    
    const handleChange = (id, field, value) => {
        setItems(prev => prev.map(item => 
            item.id === id ? { ...item, [field]: value } : item
        ));
    };
    
    const addItem = () => {
        const newId = Math.max(...items.map(item => item.id)) + 1;
        setItems(prev => [...prev, { id: newId, name: '', price: '' }]);
    };
    
    const removeItem = (id) => {
        setItems(prev => prev.filter(item => item.id !== id));
    };
    
    return (
        <div>
            <h3>商品リスト</h3>
            {items.map(item => (
                <div key={item.id} className="item-row">
                    <input 
                        type="text"
                        placeholder="商品名"
                        value={item.name}
                        onChange={(e) => handleChange(item.id, 'name', e.target.value)}
                    />
                    <input 
                        type="number"
                        placeholder="価格"
                        value={item.price}
                        onChange={(e) => handleChange(item.id, 'price', e.target.value)}
                    />
                    <button onClick={() => removeItem(item.id)}>削除</button>
                </div>
            ))}
            <button onClick={addItem}>項目を追加</button>
        </div>
    );
}

handleChangeで特定のアイテムの特定のフィールドを更新します。 addItemで新しい項目を追加し、removeItemで項目を削除します。

デバッグのコツ

onChangeが動かない場合のデバッグ方法をご紹介します。

console.logを使った確認

function DebugForm() {
    const [value, setValue] = useState('');
    
    const handleChange = (e) => {
        console.log('onChange発火:', e.target.value); // デバッグ用
        setValue(e.target.value);
    };
    
    console.log('現在のstate:', value); // デバッグ用
    
    return (
        <input 
            type="text"
            value={value}
            onChange={handleChange}
        />
    );
}

console.logでイベントの発火と値の変化を確認できます。

カスタムフックで再利用

// カスタムフック
function useForm(initialValues) {
    const [values, setValues] = useState(initialValues);
    
    const handleChange = useCallback((e) => {
        const { name, value } = e.target;
        setValues(prev => ({
            ...prev,
            [name]: value
        }));
    }, []);
    
    const reset = useCallback(() => {
        setValues(initialValues);
    }, [initialValues]);
    
    return { values, handleChange, reset };
}

// 使用例
function FormWithCustomHook() {
    const { values, handleChange, reset } = useForm({
        name: '',
        email: ''
    });
    
    return (
        <form>
            <input 
                name="name"
                value={values.name}
                onChange={handleChange}
            />
            <input 
                name="email"
                value={values.email}
                onChange={handleChange}
            />
            <button type="button" onClick={reset}>リセット</button>
        </form>
    );
}

カスタムフックを使うことで、フォームのロジックを再利用できます。

まとめ

ReactでonChangeが動かない問題について、原因と解決方法を詳しく解説しました。

主な原因と解決方法をおさらいしましょう:

  • stateの更新:必ずsetState関数を使用する
  • イベントハンドラー:関数を実行せず、関数を渡す
  • イベントオブジェクト:必ず受け取って値を取得する

効率的なフォーム管理のポイントです:

  • 複数フィールドはオブジェクトで管理
  • カスタムフックで再利用可能にする
  • バリデーションも組み込む

デバッグの手順も覚えておきましょう:

  • console.logで値の変化を確認
  • React Developer Toolsでstateを監視
  • イベントハンドラーの動作を確認

これらのポイントを押さえることで、onChangeが動かない問題を解決できます。 制御されたコンポーネントの概念を理解して、安定したフォームを作成しましょう。

ぜひ実際のプロジェクトで試してみてください!

関連記事