🌟
Reactの落とし穴:なぜpropsが変わってもstateは更新されないのか? - keyを使った優雅な解決法
はじめに
「propsが変わったらコンポーネントが再レンダリングされるんだから、stateも自動的に新しいpropsの値になるはず」
もしこう思っているなら、この記事はあなたのためのものです。実は、これはReact初心者が陥りやすい大きな誤解の一つです。今回は、この問題がなぜ起こるのか、そしてどう解決すべきかを、実例を交えて解説しま
す。
問題:編集フォームが切り替わらない!
まず、よくある問題のシナリオを見てみましょう。
// 連絡先編集フォーム
function EditContact({ contact }) {
const [name, setName] = useState(contact.name);
const [email, setEmail] = useState(contact.email);
return (
<form>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<input
value={email}
onChange={e => setEmail(e.target.value)}
/>
<button type="submit">保存</button>
</form>
);
}
// 親コンポーネント
function App() {
const [selectedContact, setSelectedContact] = useState(contacts[0]);
return (
<div>
{/* 連絡先リスト */}
{contacts.map(contact => (
<button onClick={() => setSelectedContact(contact)}>
{contact.name}
</button>
))}
{/* 編集フォーム */}
<EditContact contact={selectedContact} />
</div>
);
}
期待する動作
- "Alice"を選択 → フォームに"Alice"の情報が表示
- 名前を"Alice Smith"に編集
- "Bob"を選択 → フォームが"Bob"の情報にリセット
実際の動作
- "Alice"を選択 → フォームに"Alice"の情報が表示 ✅
- 名前を"Alice Smith"に編集 ✅
- "Bob"を選択 → フォームには"Alice Smith"が残ったまま! ❌
なぜこうなるのか?
Reactの3つの重要な仕組み
- useStateの初期値は初回レンダリング時のみ使われる
const [name, setName] = useState(contact.name);
// 初回: contact.name = "Alice" → state = "Alice"
// 2回目以降: contact.name = "Bob" → state = "Alice"のまま!
- 再レンダリングとstateの独立性
// 再レンダリングのフロー
props変更 → コンポーネント関数の再実行 → JSXの再生成
↓
でも、stateは保持される!
- Reactのコンポーネントインスタンスの同一性
// 同じ位置の同じコンポーネント = 同じインスタンス
<div>
<EditContact contact={contactA} /> {/* インスタンス1 */}
</div>
// ↓ propsが変わっても
<div>
<EditContact contact={contactB} /> {/* まだインスタンス1 */}
</div>
解決策1:useEffectを使う(非推奨)
多くの人が最初に思いつく解決策:
function EditContact({ contact }) {
const [name, setName] = useState(contact.name);
const [email, setEmail] = useState(contact.email);
// propsが変わったらstateを更新
useEffect(() => {
setName(contact.name);
setEmail(contact.email);
}, [contact.id]);
// ...
}
なぜ非推奨なのか?
- パフォーマンス: 無駄な再レンダリングが発生
- 複雑性: 同期ロジックが増える
- バグの温床: 依存配列の管理が難しい
解決策2:keyを使う(推奨)✨
Reactのkey属性を使った優雅な解決法:
function App() {
const [selectedContact, setSelectedContact] = useState(contacts[0]);
return (
<div>
{/* 連絡先リスト */}
{contacts.map(contact => (
<button onClick={() => setSelectedContact(contact)}>
{contact.name}
</button>
))}
{/* keyを追加! */}
<EditContact
key={selectedContact.id} // ← これが魔法の一行
contact={selectedContact}
/>
</div>
);
}
なぜこれで解決するのか?
key="1" のEditContact → ユーザーが編集 → key="2" に変更
↓ ↓
インスタンス1作成 インスタンス1破棄
↓
インスタンス2作成
↓
state完全リセット!
実践例:よくあるユースケース
- マルチステップフォーム
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(0);
return (
<FormStep
key={currentStep} // ステップが変わるとリセット
step={currentStep}
onNext={() => setCurrentStep(currentStep + 1)}
/>
);
}
- 詳細画面の切り替え
function ProductDetails({ productId }) {
return (
<ProductForm
key={productId} // 商品が変わるとフォームリセット
productId={productId}
/>
);
}
- モーダルの再利用
function App() {
const [editingItem, setEditingItem] = useState(null);
return (
<>
{editingItem && (
<EditModal
key={editingItem.id} // 編集対象が変わるとリセット
item={editingItem}
onClose={() => setEditingItem(null)}
/>
)}
</>
);
}
よくある誤解と注意点
誤解1:keyは配列のmapでしか使わない
// ❌ 間違い:keyは配列専用と思っている
{items.map(item => <Item key={item.id} />)}
// ✅ 正解:単一コンポーネントでも使える
<EditForm key={formId} />
誤解2:keyを使うとパフォーマンスが悪い
実は逆です!
// useEffectの場合:レンダー → DOM更新 → effect実行 → 再レンダー
// keyの場合:古いコンポーネント破棄 → 新規作成 → レンダー(1回だけ)
注意点:keyの乱用
// ❌ 悪い例:毎回新しいkey
<Component key={Math.random()} /> // 毎回リセットされる!
// ❌ 悪い例:インデックスをkey
<Component key={index} /> // 順序が変わると予期せぬリセット
// ✅ 良い例:安定したID
<Component key={item.id} />
パフォーマンス比較
// 測定コード
function PerformanceTest() {
console.time('render');
// useEffect版
const WithEffect = () => {
useEffect(() => {
console.log('Effect fired');
// state更新...
}, [prop]);
};
// key版
const WithKey = () => {
console.log('Component created');
// 初期化処理...
};
console.timeEnd('render');
}
// 結果:
// useEffect版: render 2ms → Effect fired → render 1ms(計3ms)
// key版: Component created → render 2ms(計2ms)
まとめ:Reactの思想を理解する
keyを使うべき理由
- 宣言的: 「このIDが変わったら新しいフォーム」という意図が明確
- シンプル: 追加のロジック不要
- 確実: stateの完全リセットが保証される
- Reactらしい: フレームワークの仕組みを活用
覚えておくべきこと
- propsの変更 ≠ stateの変更
- 同じ位置の同じコンポーネント = 同じインスタンス
- key属性はコンポーネントの同一性を制御する
おわりに
Reactでフォームやエディタを実装する際、「propsが変わったのにstateが更新されない」という問題は必ず遭遇します。このとき、反射的にuseEffectに手を伸ばすのではなく、まずkey属性での解決を検討してみてく
ださい。
多くの場合、それがもっともシンプルで、もっともReactらしい解決策になるはずです。
この記事で紹介したパターンは、React公式ドキュメントの「そのエフェクトは不要かも」でも推奨されています。より深く学びたい方は、ぜひ公式ドキュメントも参照してください。
参考
Discussion