useStateと親子連携:propsとstateの分離再検討
Reactでは「状態は上位に持つべき」とされることが多い一方で、親から渡された props
を useState
にコピーして使う設計が散見されます。このような構造は一見自然に見えますが、状態の整合性や同期性に齟齬が出やすく、レビューアーとして注意深く読み解く必要があります。
本稿では、useState
を用いて親子連携を実装する際に生じやすい構造的問題と、それに対するレビュー指針を整理します。
典型的な構造:propsからstateへのコピー
function UserForm({ user }: { user: User }) {
const [name, setName] = useState(user.name);
const [age, setAge] = useState(user.age);
return (
<>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={age} onChange={e => setAge(Number(e.target.value))} />
</>
);
}
@Reviewer`user` から初期値として `useState` にコピーしていますが、その後 `user` が変化しても `name` や `age` は同期されません。親の更新が反映されない構造になっており、同期性の不整合を引き起こします。
このような「propsからstateへコピーする構造」は、初期化のつもりで行われることが多いですが、親の値が変わっても反映されない構造になります。
親子の状態を“同期させる”べきなのか?
Reactでは、「propsは読み取り専用」「stateはローカルで持つ」ことが原則です。したがって、propsからstateにコピーする構造が必要なケースは限られており、安易な設計は状態の二重管理を招きます。
以下のような判断がレビュー観点として重要です。
- 子コンポーネントが値を変更する必要があるか?
- 親が変更した値を子でも即座に反映すべきか?
- 編集後の状態を親にどう伝えるか?
これらの答えが曖昧なまま useState
を使うと、同期性が崩壊する構造になります。
よくある誤解:stateは必要ない
多くの実装者が「propsで受け取った値を useState
に入れるのが当然」と思っていますが、本来はpropsを直接使うことで整合性が保たれます。
const [mode, setMode] = useState(props.mode);
if (props.mode === "admin") {
// ...
}
%% @Reviewer
%% props.mode
を useState
にコピーせず、必要に応じて直接参照すれば、同期性の問題を回避できます。値を書き換える必要がない場合はstate化すべきではありません。
useEffectでprops変更を追跡してstateに反映する構造
「propsからstateへコピーしたが、propsの変化も反映したい」という要求がある場合、useEffect
を使って更新を追跡する構造が選ばれます。
useEffect(() => {
setName(user.name);
setAge(user.age);
}, [user]);
@Reviewer`user` の変更を検知してstateを更新していますが、これは同期の責務を手動で担保している構造です。propsが頻繁に変わるような設計では、意図しない再設定によって入力中の値が上書きされる可能性もあります。
この構造は一見問題を解決しているように見えますが、入力中に親の更新が入るとユーザーの操作が破壊されるなど、副作用を伴いやすいです。
リフトアップと状態委譲の使い分け
レビュー観点では、「状態を上位で持たせて渡す(リフトアップ)」のか、「ローカルに持たせて結果だけ渡す(状態委譲)」のかを判断する必要があります。
状態を上位で持たせるべきケース
- 子が状態を編集するが、親がそれを即時に監視したい
- 複数のコンポーネント間で同一の状態を共有したい
- ローカルに持たせると更新のたびにUI全体が破綻する
ローカルに持たせるべきケース
- 入力途中の状態を一時的に保持したい
- 編集と確定のフェーズが明確に分かれている
- 最終的に
onSave
などでまとめて通知できればよい
子コンポーネントで状態を管理しつつ、最終的に親に通知する例
function EditUserForm({ initialUser, onSave }: Props) {
const [name, setName] = useState(initialUser.name);
const [age, setAge] = useState(initialUser.age);
const handleSubmit = () => {
onSave({ name, age });
};
return (
<>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={age} onChange={e => setAge(Number(e.target.value))} />
<button onClick={handleSubmit}>保存</button>
</>
);
}
@Reviewer初期値を受け取りつつ、編集中の状態は子コンポーネント内で保持し、保存時に親へ通知する構造です。編集フェーズが分離されており、propsとの同期を避ける設計になっています。
このように、編集ローカル・通知イベント方式は多くのフォーム系UIで推奨されるパターンです。
まとめ:propsとstateの分離に関する判断軸
propsからstateを生成する構造に遭遇した場合、レビューアーは次のような観点で読み解くことが求められます。
- propsが一時的な編集用か、読み取り専用かを判断
- 子コンポーネントが状態を上書きしてよい設計かを確認
- propsの変更をstateへ反映する構造が適切かを評価
- 編集ローカル+保存通知の構造で問題が解決するかを検討
propsとstateの関係性が不明瞭な設計は、UIの同期性を壊すだけでなく、ロジックの責務境界を曖昧にします。レビューでは「本当にそのstateが必要なのか?」という問いから始め、構造全体の責任の所在を見直すことが鍵になります。