🐥

ReactのinputとStateで「controlled/uncontrolled」エラーを解決する実践ガイド

に公開

はじめに

Reactでフォームを作成していると、こんなエラーに遭遇したことはありませんか?

A component is changing an uncontrolled input to be controlled. 
This is likely caused by the value changing from undefined to a defined value, which should not happen.

この記事では、実際の開発で起こりがちなinputとStateの問題を、具体的なコード例とともに解決方法を紹介します。

問題の発生パターン

パターン1: valueが設定されていない

// ❌ よくある間違い
<input 
    type="text" 
    id="username" 
    name='username' 
    // value={userInfo.username} ← これが抜けている
    onChange={(e) => setUserInfo({...userInfo, username: e.target.value})}
/>

パターン2: ラジオボタンの一部だけcontrolled

// ❌ 20代だけcontrolled、他はuncontrolled
<input type="radio" name="userage" checked={userInfo.userage === "20"} />
<input type="radio" name="userage" value="30" />  // checkedがない
<input type="radio" name="userage" value="40" />  // checkedがない

解決方法

1. 基本的なtext inputの正しい書き方

const [userInfo, setUserInfo] = useState({
    username: "",
    usermail: "",
    userage: "",
});

// ✅ 正しい書き方
<input 
    type="text" 
    id="username" 
    name='username' 
    value={userInfo.username}  // ← 必須!
    onChange={(e) => setUserInfo({...userInfo, username: e.target.value})}
/>

2. ラジオボタンの効率的な実装

繰り返しコードを避けるため、配列とmapを使用します:

// 選択肢を配列で定義(関数の外)
const ageOptions = [
    { value: "20", label: "20代" },
    { value: "30", label: "30代" },
    { value: "40", label: "40代" },
    { value: "50", label: "50代" },
    { value: "60", label: "60代" },
    { value: "over", label: "それ以上" }
];

const handleAgeChange = (value) => {
    setUserInfo({
        ...userInfo,
        userage: value
    });
};

// JSX
<div>
    {ageOptions.map((option) => (
        <label key={option.value} htmlFor={`userage-${option.value}`}>
            <input 
                type="radio" 
                id={`userage-${option.value}`}
                name="userage" 
                value={option.value}
                checked={userInfo.userage === option.value}
                onChange={(e) => handleAgeChange(e.target.value)}
            />
            <span>{option.label}</span>
        </label>
    ))}
</div>

3. 必須項目のバリデーション

// リアルタイムエラー表示
{!userInfo.userage && (
    <p className="text-red-500 text-sm">年齢の選択は必須です</p>
)}

Computed Property Names(計算プロパティ名)

複数の質問がある場合、動的なキーでstateを管理します:

const [answers, setAnswers] = useState({});

const handleAnswerChange = (questionId, selectedOption) => {
    setAnswers({
        ...answers,
        [questionId]: selectedOption  // ← []が必要!
    });
};

// 使用例
{data.questions.map((question) => (
    <div key={question.id}>
        <h2>{question.id}. {question.question}</h2>
        {question.options.map((option) => (
            <label key={option.id}>
                <input 
                    type="radio"
                    name={`question-${question.id}`}
                    value={option.id}
                    checked={answers[question.id] === option.id}
                    onChange={() => handleAnswerChange(question.id, option.id)}
                />
                {option.text}
            </label>
        ))}
    </div>
))}

なぜ[questionId]に[]が必要?

// ✅ []があると変数の値をキーに使用
const questionId = "1";
setAnswers({
    ...answers,
    [questionId]: selectedOption  // 結果: { "1": "a" }
});

// ❌ []がないと文字通り"questionId"がキーになる
setAnswers({
    ...answers,
    questionId: selectedOption  // 結果: { "questionId": "a" }
});

ベストプラクティス

1. 定数は関数の外で定義

// ✅ パフォーマンスが良い
const ageOptions = [...];  // 関数の外

export default function Component() {
    // ロジックに集中
}

2. keyは意味のある値を使用

// ✅ 推奨
key={option.value}  // "male", "female", "other"

// ❌ 避ける
key={index}  // 0, 1, 2

3. keyは最も外側の要素に設定

// ✅ 正しい
{options.map((option) => (
    <label key={option.value}>  {/* 最も外側 */}
        <input ... />
    </label>
))}

// ❌ 間違い
{options.map((option) => (
    <div>  {/* keyがない */}
        <label key={option.value}>  {/* 内側にある */}

まとめ

  • controlled input: valueonChangeの両方が必要
  • ラジオボタン: 同じnameグループは全て同じ方式で統一
  • 効率化: 配列とmapで繰り返しコードを削減
  • 動的キー: [変数名]でComputed Property Namesを活用
  • パフォーマンス: 定数は関数外で定義

これらのパターンを覚えておけば、Reactのフォームで困ることは大幅に減るでしょう!

参考リンク

Discussion