🌟

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>
  );
}

期待する動作

  1. "Alice"を選択 → フォームに"Alice"の情報が表示
  2. 名前を"Alice Smith"に編集
  3. "Bob"を選択 → フォームが"Bob"の情報にリセット

実際の動作

  1. "Alice"を選択 → フォームに"Alice"の情報が表示 ✅
  2. 名前を"Alice Smith"に編集 ✅
  3. "Bob"を選択 → フォームには"Alice Smith"が残ったまま! ❌

なぜこうなるのか?

Reactの3つの重要な仕組み

  1. useStateの初期値は初回レンダリング時のみ使われる
const [name, setName] = useState(contact.name);
// 初回: contact.name = "Alice" → state = "Alice"
// 2回目以降: contact.name = "Bob" → state = "Alice"のまま!
  1. 再レンダリングとstateの独立性
// 再レンダリングのフロー
props変更 → コンポーネント関数の再実行 → JSXの再生成
                    ↓
              でも、stateは保持される!
  1. 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]);

  // ...
}

なぜ非推奨なのか?

  1. パフォーマンス: 無駄な再レンダリングが発生
  2. 複雑性: 同期ロジックが増える
  3. バグの温床: 依存配列の管理が難しい

解決策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完全リセット!

実践例:よくあるユースケース

  1. マルチステップフォーム
function MultiStepForm() {
  const [currentStep, setCurrentStep] = useState(0);

  return (
    <FormStep 
      key={currentStep}  // ステップが変わるとリセット
      step={currentStep}
      onNext={() => setCurrentStep(currentStep + 1)}
    />
  );
}
  1. 詳細画面の切り替え
function ProductDetails({ productId }) {
  return (
    <ProductForm
      key={productId}  // 商品が変わるとフォームリセット
      productId={productId}
    />
  );
}
  1. モーダルの再利用
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を使うべき理由

  1. 宣言的: 「このIDが変わったら新しいフォーム」という意図が明確
  2. シンプル: 追加のロジック不要
  3. 確実: stateの完全リセットが保証される
  4. Reactらしい: フレームワークの仕組みを活用

覚えておくべきこと

  • propsの変更 ≠ stateの変更
  • 同じ位置の同じコンポーネント = 同じインスタンス
  • key属性はコンポーネントの同一性を制御する

おわりに

Reactでフォームやエディタを実装する際、「propsが変わったのにstateが更新されない」という問題は必ず遭遇します。このとき、反射的にuseEffectに手を伸ばすのではなく、まずkey属性での解決を検討してみてく
ださい。

多くの場合、それがもっともシンプルで、もっともReactらしい解決策になるはずです。


この記事で紹介したパターンは、React公式ドキュメントの「そのエフェクトは不要かも」でも推奨されています。より深く学びたい方は、ぜひ公式ドキュメントも参照してください。

参考

https://ja.react.dev/learn/you-might-not-need-an-effect

Discussion