こんにちは!Reactの学習、楽しく進めていますか?
画面がスイスイ動くようになってくると、「おっ、私にもできるぞ!」というワクワク感がたまりませんよね。
でも、Reactを触り始めて多くの人が「あれ……?」と最初に立ち止まる、ちょっとした魔物のような罠があるんです。それが今回お話しする「propsの変更をそのままstateに同期させようとして、頭がこんがーる問題」です。
大丈夫ですよ、最初はみんなここで盛大にハマります。私も昔、何度も夜を徹してコードと睨めっこしました。
今回は、なぜこれが「やっちゃいけないアンチパターン」なのか、そしてどうすればスマートに解決できるのかを、身近な例えを交えながら優しく解きほぐしていきましょう!
—
1. カフェの注文カウンターで考えてみよう
まずはイメージしやすいように、身近な例え話をさせてください。
あなたは今、大人気のカフェでレジ打ちのアルバイトをしています。
お客さん(親コンポーネント)がレジにやってきて、「アイスコーヒーを一つ!」と注文してくれました(これが props です)。
あなたはそれを聞いて、手元のメモ用紙(これが子コンポーネントの state)に「アイスコーヒー」と書き込みました。ここまでは順調です。
やりがちな「大惨事」の始まり
ところが、お客さんが行列の途中で急に気を変わり、「あ、やっぱホットティーにして!」と言い出しました(propsの変更)。
ここで、もしあなたが「お客さんが注文を変えたんだから、私のメモ用紙も勝手に書き換えなきゃ!」と、メモ用紙の管理を自分で頑張ろうとすると……どうなるでしょう?
お客さんの口頭での変更と、あなたがメモを書き直すタイミング、そしてすでに作り始めていたドリンクの指示がごちゃ混ぜになり、レジカウンターは大パニックです。
Reactの世界でもこれと全く同じことが起きます。
「親から渡されたデータ(props)の変化を監視して、子自身のメモ(state)を無理やり書き換えようとする」。これが、バグを生む最大の原因なんです。
—
2. なぜ「propsをstateにコピーする」とダメなの?
コードでよく見かける、こんな書き方を思い出してみてください(あるいは、まさに今書こうとしていませんか?)。
// ❌ やりがちなアンチパターン
function ChildComponent({ initialCount }) {
// propsを初期値としてstateに入れる
const [count, setCount] = useState(initialCount);
// propsが変わったときにstateも無理やり同期させようとする(useEffectの悪用)
useEffect(() => {
setCount(initialCount);
}, [initialCount]);
return
現在のカウント: {count}
;
}
一見すると、「親の数字が変わったら子も連動して変わりそう」で良さそうに見えますよね。
でも、これには大きな問題が2つあります。
1. 「一人の人間が2つの財布を持っている」状態になる
データの情報源(Single Source of Truth)が、親の持つ `initialCount` と、子の持つ `count` の2つに分裂してしまいます。「どっちが今の正しい数字なの?」とReactも私たちも迷子になってしまいます。
2. 非同期のタイミングズレでバグる
Reactの状態更新(useState)は非同期で行われます。そのため、「propsが変わった!すぐstateを更新しなきゃ!」という焦りが、予期せぬ無限ループや古いデータの表示(ステートのズレ)を引き起こすんです。
「親からデータをもらうなら、子でそれを自分の勝手な持ち物にしない」。これがReactの鉄則です。
—
3. じゃあどうするの?解決策①:計算できるものは「その場で計算する」
もし、子コンポーネントで表示したいデータが、親からもらったpropsをもとにパパッと計算できるものなら、そもそもstate(専用のメモ用紙)を作る必要すらないんです。
カフェの例で言えば、メモ用紙にわざわざ書き写さなくても、お客さんの注文票をそのまま見ればいいよね、というお話です。
💡 解決コード例
// ⭕️ 正しいアプローチ:stateを持たず、propsをそのまま使う
function PriceTag({ unitPrice, quantity }) {
// stateを使わず、propsからその場で合計金額を計算する
// これなら「同期ズレ」なんて起きようがありません!
const totalPrice = unitPrice quantity;
return (
単価: {unitPrice}円
数量: {quantity}個
合計金額: {totalPrice}円
);
}
「stateを持たないなんて不安……」と思うかもしれませんが、「変化するデータをstateに入れなくていいなら、入れないのが一番安全で美しいコード」です。これを覚えておいてくださいね。
—
4. 解決策②:「どうしてもリセットしたい」ときは `key` を使う
「いやいや、子コンポーネントが自分自身でフォームの入力途中などの内部状態(state)を持っていて、親から新しいIDやデータが渡されたら、初期状態からきれいにリセットしてほしいんだ!」
そんなシーンもありますよね。ユーザーのプロフィール編集画面などを想像してください。別の人に切り替えた瞬間、前の人の入力データが残っていたら大惨事です。
ここで先ほどの `useEffect` で無理やり同期させるのではなく、Reactの奥義「key属性の変更によるコンポーネントの作り直し(マウントし直し)」を使います。
Reactは、要素の `key` が変わると、「あ、中身が全然違う新しいパーツに変わったんだな」と認識し、古いパーツを一度きれいに消して、新しいパーツをゼロから作り直してくれます。
💡 解決コード例
import { useState } from ‘react’;
// 子コンポーネント:自分の中にテキストの入力状態(state)を持っている
function ProfileEditor({ userId, defaultName }) {
// propsの defaultName は「あくまで初期値」として受け取る
const [name, setName] = useState(defaultName);
return (
ユーザーID: {userId}
setName(e.target.value)}
className=”border p-1 rounded mt-2″
/>
);
}
// 親コンポーネント
function ParentApp() {
const [currentUserId, setCurrentUserId] = useState(1);
// ユーザーを切り替えるボタンのデータ
const users = [
{ id: 1, name: ‘田中さん’ },
{ id: 2, name: ‘佐藤さん’ },
];
return (
{/
★ここがポイント!
keyに currentUserId を渡すことで、IDが変わった瞬間に
ProfileEditor ごとキレイに破棄され、新しい初期値で生まれ変わります。
/}
);
}
この方法を使うと、`useEffect` でこねくり回さなくても、Reactのライフサイクルの仕組みに自然に乗っ取って、安全に初期化を行うことができます。初めてこれを知ったときは、私も「なるほど、こうやってコンポーネントを使い捨てるのか!」と目からウロコが落ちました。
—
最後に:つまずいても、一歩ずつ進めば大丈夫
Reactの状態管理は、最初は本当に頭が混乱します。
「あれ?なんでさっきの値にならないんだ?」と手が止まる日もあるでしょう。でも、それはあなたがしっかりとコードと向き合っている証拠です。
もしまた「propsの変更をstateに反映させなきゃいけない気がする……」と思ったら、今日のこの記事をそっと思い出してみてください。
1. そもそも計算で求められないか?(stateを削る)
2. リセットしたいだけなら、`key` でコンポーネントごと作り直せないか?
この2つの引き出しを持っていれば、大抵のUIの悩みは綺麗に解決できます。
あなたのReactライフが、少しでも軽やかで楽しいものになりますように。一緒に一歩ずつ進んでいきましょう!

コメント