propsをstateに同期させる「useEffect」という禁断の果実を、どう料理するか
現場でコードレビューをしていると、必ずと言っていいほどぶつかる壁がある。「親から渡ってきたpropsが変わったら、子コンポーネント内のstateも追従させたい」という要求だ。
多くの駆け出しエンジニアは、迷わず`useEffect`に手を伸ばす。「ああ、`props.value`を依存配列に入れればいいんだな」と。確かに動く。だが、それはReactの宣言的なデータフローを自ら破壊し、地雷原を歩いているようなものだ。
今日は、この「propsからstateへの同期」というテーマについて、なぜそれが危険なのか、そして現場で生き残るための「正攻法」を叩き込む。
—
なぜuseEffectでの同期が「悪手」なのか
Reactは、データが親から子へと一方向に流れる仕組みだ。stateは「そのコンポーネントが自律的に管理する状態」を指す。それに対し、propsは「親から押し付けられた外部の事実」だ。
`useEffect`で`props`を監視して`setState`を叩くということは、「事実(props)」を「コピー(state)」して、そのコピーを「事実」として扱おうとする行為だ。これには以下の弊害がある。
1. 二重のレンダリング: propsが変わる → レンダリング → useEffect発火 → setState → 再レンダリング。この無駄な再レンダリングは、複雑なUIでは確実にパフォーマンスを蝕む。
2. 不整合の温床: 「どちらが正解のデータなのか」という境界が曖昧になり、コンポーネントの挙動が予測不可能になる。
3. 無限ループの罠: 依存配列の指定を少し間違えただけで、ブラウザがフリーズする悪夢を見ることになる。
—
戦略1:そもそも「同期」を避ける(State Lifting)
もし、そのstateが単にpropsをコピーしているだけなら、そもそもstateを作るな。
propsをそのまま使うか、計算が必要ならレンダリング中に算出(派生状態)すればいい。Reactのレンダリングは非常に高速だ。わざわざstateに保存してメモリを浪費する必要はない。
// 悪い例:propsをわざわざstateに同期させている
const [value, setValue] = useState(props.value);
useEffect(() => { setValue(props.value); }, [props.value]);
// 良い例:派生状態として扱う
const derivedValue = props.value.toUpperCase(); // レンダリング中に計算するだけ
—
戦略2:どうしても必要な場合の「キーによるリセット」
「親からのpropsが変わったタイミングで、内部のstateを完全に初期化したい」というケースは、実務でもたまにある(例:ユーザーIDが変わったらフォームを空にする)。
ここで`useEffect`を書いてはいけない。`key`属性を使え。
Reactにおいて`key`は、コンポーネントのIDだ。これが変われば、Reactは古いコンポーネントを完全に破棄し、新しいコンポーネントとしてマウントし直す。
// 親コンポーネント側
// userIDが変わるたびに、ChildComponentは別物として再生成される
// 子コンポーネント側
// useEffectで同期なんて考えなくていい。初期値として素直に受け取るだけ。
const ChildComponent = ({ initialValue }) => {
const [value, setValue] = useState(initialValue);
// これで、keyが変わればコンポーネントが再マウントされ、valueも自然と初期化される
return setValue(e.target.value)} />;
};
これが最もクリーンで、Reactの設計思想に則ったやり方だ。
—
戦略3:どうしても「同期」が必要な例外(制御されたコンポーネント)
UIライブラリを作っている時など、どうしてもpropsを内部状態と「同期」させなければならない例外がある。その際は、React公式も認める「制御されたコンポーネント」のパターンを使う。
もし`useEffect`を使うなら、以下のルールを徹底してくれ。
import { useState, useEffect } from ‘react’;
const ControlledInput = ({ value: propValue }) => {
const [internalValue, setInternalValue] = useState(propValue);
// propsの変更を検知して同期する
// この時、現在のinternalValueと異なる場合のみ更新するようにガードを入れること
useEffect(() => {
if (propValue !== internalValue) {
setInternalValue(propValue);
}
}, [propValue, internalValue]); // 依存関係は適切に
return (
setInternalValue(e.target.value)}
/>
);
};
ここでのポイント:
- ガード句を入れる: `propValue !== internalValue` というチェックがないと、無限再レンダリングや不要な更新が走る。
- 信頼の源泉(Source of Truth)を意識: 「どっちが今の正しい状態か」を常に自分に問い直すこと。
—
最後に:シニアからのアドバイス
「`useEffect`で同期する」というコードを書きたくなったら、それは「設計がどこかで破綻しているサイン」だと思ってほしい。
まずは「stateを捨てられないか?」「key属性でリセットできないか?」を検討する。それでもダメなら、初めて`useEffect`というメスを振るう。この順序を叩き込んでおけば、君の書くコンポーネントは劇的に安定するはずだ。
Reactは「変化」を追うライブラリではない。「状態」から「UI」を導き出す関数だ。その原則さえ忘れなければ、どんな複雑なUIも怖くはない。現場で困ったときは、またいつでも聞きに来てくれ。

コメント