React のメンタルモデル: Dan Abramov 流の考え方
React のメンタルモデルを身につけると React がシンプルに感じられます。コンポーネント、レンダー、状態、エフェクトの主要な考え方を学び、チャットで高速に UI を作るときに活用しましょう。

正しいメンタルモデルがないと React は難しく感じる
React は最初イライラすることがあります。UI が変わるのは見えるのに、なぜ変わったのか説明できないことがあるからです。ボタンをクリックして何かが更新され、別の部分が驚くように変わる──これはたいてい「React が変」なのではなく、「自分の中の React のイメージがぼんやりしている」だけです。
メンタルモデルとは、ものごとがどう動くかについて自分が語る単純な物語です。その物語が間違っていると、自信を持って決めたことが混乱した結果を招きます。温度調節器を考えてみてください: 悪いモデルは「22°C に設定したら部屋が瞬時に 22°C になる」です。よりよいモデルは「目標温度を設定すると、ヒーターがオン・オフを繰り返しながら時間をかけてそこに近づける」です。より良い物語を持てば、振る舞いはランダムには感じなくなります。
React も同じです。いくつかの明確な考え方を採用すれば、React は予測可能になります: 現在のデータを見れば画面に何が出るかを信頼して推測できるようになります。
Dan Abramov はこの「予測可能にする」マインドセットを広めるのに貢献しました。目的はルールを暗記することではなく、頭の中に少数の真実を持ってデバッグを推論で行えるようにすることです。
覚えておくべき考え方:
- UI は状態の結果であって、手作業の DOM 編集の集合ではない。
- コンポーネントはその入力に対して UI がどう見えるべきかを記述する関数である。
- データは下へ流れ、変更はイベントを通じて上へ上がる。
- 副作用(effects)はレンダーとは分離され、特定の目的のためにある。
これらを心に留めておけば、React は魔法ではなく信頼できるシステムのように感じられます。
コンポーネント、props、state: 中核の絵
「画面」単位で考えるのをやめ、小さな部品で考え始めると React は簡単になります。コンポーネントは再利用可能な UI の単位です。入力を受け取り、その入力に対して UI がどう見えるかを返します。
コンポーネントを純粋な記述として扱うと良いです: 「このデータがあればこれを表示する」。その記述はどこで使われてもよく、居場所に依存しません。
Props は入力です。親コンポーネントから来ます。Props はコンポーネントに「所有」されるものではなく、コンポーネントが勝手に書き換えるべきものでもありません。ボタンが label="Save" を受け取ったら、そのボタンの仕事はそのラベルを表示することであり、自分で別のものに変えることではありません。
State は所有されるデータです。コンポーネントが時間を通じて覚えているものです。ユーザーの操作、リクエストの完了、あるいは何かを変えるという判断で状態は変わります。Props と違って、state はそのコンポーネント(あるいはあなたが所有させたいコンポーネント)に属します。
キーアイデアの簡潔な版: UI は state の関数です。state が「loading」ならスピナーを表示する。state が「error」ならメッセージを表示する。state が「items = 3」なら三行をレンダーする。あなたの仕事は UI を state から読み取らせ、隠れた変数でふらつかないようにすることです。
概念を分ける簡単な方法:
- コンポーネント: UI のかけら (
SearchBox,ProfileCard,CheckoutForm) - Props: 表示に必要なもの (
name,price,disabled) - State: 覚えていること (
isOpen,query,selectedId)
例: モーダル。親は title や onClose を props として渡せます。モーダルは isAnimating を state として持つかもしれません。
チャット経由で UI を生成する場合でも(たとえば Koder.ai 上で)、この分離は理性を保つ最速の方法です: まず何が props で何が state かを決め、それから UI を従わせます。
レンダリングと再レンダリング: 実際に何が変わるのか
頭の中で React を保つ役に立つ考え方(Dan Abramov の精神に近いのは): レンダリングは計算であって、描画作業ではない、です。React はコンポーネント関数を実行して、現在の props と state に対して UI がどう見えるべきかを決めます。その出力は画面の説明であって、ピクセルではありません。
再レンダーは単にその計算を繰り返すことを意味します。決して「ページ全体が再描画される」ではありません。React は新しい結果を以前のものと比較し、実際の DOM に対して最小限の変更セットを適用します。多くのコンポーネントが再レンダーされても、実際に更新される DOM ノードは少数かもしれません。
ほとんどの再レンダーは単純な理由で起きます: コンポーネントの state が変わった、props が変わった、あるいは親が再レンダーされて React が子に再レンダーを求めた、などです。最後のケースは人を驚かせますが、通常は問題ありません。レンダーを「安くて退屈なもの」と扱えば、アプリはより推論しやすくなります。
この考え方を保つための経験則: レンダーを純粋に保つこと。同じ入力(props + state)が与えられれば、コンポーネントは同じ UI 記述を返すべきです。レンダーに驚きを持ち込まないでください。
具体例: Math.random() で ID をレンダー内で生成すると、再レンダーのたびに変わり、チェックボックスがフォーカスを失ったり、リストアイテムが再マウントされたりします。ID は一度だけ作る(state、memo、コンポーネント外)ことでレンダーは安定します。
覚えておくべき一文: 再レンダーは「UI がどうあるべきかを再計算する」ことであって、「すべてを作り直す」ことではありません。
状態の更新: 変更は小さく予測可能に
もうひとつ役に立つモデル: state の更新はリクエストであって即時代入ではない、ということです。setCount(count + 1) のように呼ぶと、React に新しい値でレンダーをスケジュールするよう頼んでいるだけです。直後に state を読んでも古い値が見えることがあります。React がまだレンダーしていないからです。
だから「小さく予測可能な」更新が重要です。現在の値を勝手に拾って使うのではなく、変化を記述するようにしましょう。次の値が前の値に依存する場合はアップデータ形式を使ってください: setCount(c => c + 1)。これは React の動きに合っています: 複数の更新がキューに入り、順に適用されます。
不変性(immutability)はそのもう半分です。オブジェクトや配列をその場で変更しないでください。変更を含む新しいものを作りましょう。React は「この値は新しい」と見てくれますし、あなたの頭も何が変わったかを追跡できます。
例: todo アイテムのトグル。安全な方法は新しい配列を作り、変更した todo のみ新しいオブジェクトにすることです。危険な方法は既存の配列内で todo.done = !todo.done をやってしまうことです。
また state は最小限に保つこと。よくある罠は計算できる値を保存してしまうことです。すでに items と filter があるなら、filteredItems を state に保存しないでください。レンダー中に計算しましょう。state が少なければ少ないほど、値がずれる道は減ります。
state に入れるべきものの簡単なテスト:
- 時間とともに変わり、UI が反応する必要があるなら保存する。
- 他の state や props から導出できるなら保存しない。
- 同じ真実を二か所に複製しない。
チャットで UI を作るなら(Koder.ai を含む)、変更は小さなパッチで頼むとよい: 「真偽値フラグを一つ追加」や「このリストを不変に更新する」など。小さく明示的な変更は生成器とあなたの React コードの整合性を保ちます。
エフェクトは同期のために、基本的な UI ロジックのためではない
レンダーは UI を記述します。エフェクトは外の世界と同期します。「外」とは React が制御しないもの: ネットワーク、タイマー、ブラウザ API、そして時には命令的な DOM 作業です。
もし何かが props と state から計算できるなら、それはたいていエフェクトに置くべきではありません。エフェクトに置くと二段階(レンダー→エフェクト実行→state 設定→再レンダー)になりがちです。その余分な一歩がチラつき、ループ、あるいは「なぜこれが古いのか?」というバグの原因になります。
よくある混乱: firstName と lastName があって、fullName をエフェクトで state に入れている場合です。しかし fullName は副作用ではありません。導出データです。レンダー中に計算すれば常に一致します。
習慣として: UI の値はレンダー中(または本当に高価なら useMemo)で導出し、エフェクトは「何かをする」仕事(フェッチ、購読、タイマー、DOM 操作)に使いましょう。
依存配列はトリガーの一覧
依存配列は「これらの値が変わったら外の世界と再同期する」という視点で扱ってください。パフォーマンスのテクニックでも、警告を押さえる場所でもありません。
例: userId が変わったときにユーザー詳細を取得するなら、userId は依存配列に入ります。エフェクトが token を使うならそれも含めないと、古いトークンでフェッチしてしまう恐れがあります。
良い感覚チェック: エフェクトを削除すると UI が間違うだけなら、それは本当のエフェクトではないかもしれません。削除するとタイマーが止まる、購読が解除される、フェッチが省かれるようなものなら、それはおそらくエフェクトにふさわしい仕事です。
データフロー: 分散した state より一つの真実
最も役に立つメンタルモデルの一つは単純です: データは木構造の下へ流れ、ユーザー操作は関数を通じて上へ上がる。
親は値を子へ渡します。子が同じ値を二重に「所有」してはいけません。子は変更を要求するために関数を呼び、親が新しい値を決めます。
2 つの UI 部分が一致する必要があるときは、値を一か所に保存して下へ渡してください。これが「state を上げる(lifting state)」です。余計な配管に感じるかもしれませんが、もっと悪い問題を防ぎます: 2 つの state がずれて、同期させるためのハックを増やすことになってしまいます。
例: 検索ボックスと結果リスト。もし入力が独自にクエリを保持し、リストも独自にクエリを持つなら、最終的に「入力は X を表示しているがリストは Y を使っている」という事態になります。修正策は query を親に置き、両方に渡し、入力には onChangeQuery(newValue) ハンドラを渡すことです。
ただし state を上げることが常に正解というわけではありません。値が一つのコンポーネント内部だけで意味を持つなら、そのまま内部に置いておく方が読みやすいことが多いです。
実用的な境界線:
- ローカル state: UI の細部(ドロップダウンの開閉、どのタブがハイライトされているか)
- 共有 state: 複数のコンポーネントが合意する必要があるデータ(フィルタ、選択項目、認証状態)
迷ったら次のサインを探してください: 二つのコンポーネントが同じ値を違う方法で表示している; ある場所のアクションが遠くの何かを更新する必要がある; 「念のため」に props を state にコピーしている; 二つの値を揃えるためにエフェクトを書いている──これらは state を持ち上げるべきシグナルです。
このモデルは Koder.ai のようなチャットツールで作るときにも役立ちます: 共有 state ごとに単一の所有者を決め、ハンドラは上へ流れるように生成させましょう。
ステップバイステップ: まず state を描いて機能を設計する
手に収まるサイズの機能を選びます。良い例は、検索可能なリストで項目をクリックすると詳細をモーダルで見る、というものです。
まず UI の部品と起こりうるイベントをスケッチします。まだコードのことは考えずに、ユーザーが何をできるか、何が見えるかを考えてください: 検索入力、リスト、選択行のハイライト、モーダルがあります。イベントは入力でのタイピング、項目のクリック、モーダルの開閉です。
次に「state を描く」。保存すべき少数の値を書き出し、誰が所有するか決めます。良いルール: その値を必要とするすべての場所の最も近い共通の親が所有するべきです。
この機能なら保存する state は小さくて済みます: query (文字列)、selectedId (id または null)、isModalOpen (真偽)。リストは query を読み取り項目をレンダーします。モーダルは selectedId を読んで詳細を表示します。リストとモーダルが両方 selectedId を必要とするなら、両方に渡すために親が保持します。
次に、導出データと保存データを分けます。フィルタ後のリストは導出です: filteredItems = items.filter(...)。これを state に保存しないでください。items と query からいつでも再計算できます。導出データを保存すると値がずれる原因になります。
そのあとで、エフェクトが必要かを考えます。項目がすでにメモリにあるなら不要です。タイプされたクエリで結果をフェッチするなら必要です。モーダルの閉鎖で何かを保存するなら必要です。エフェクトは同期(フェッチ、保存、購読)に使い、基本的な UI 配線には使いません。
最後にいくつかのエッジケースでフローをテストします:
- 速くタイプしてから消したとき、リストは元に戻るか?
- 項目をクリックして選択した後、検索でリストが変わったとき
selectedIdはまだ有効か? - モーダルを閉じたら選択を維持するか、リセットするか?
紙の上で答えられれば、React のコードは通常簡単です。
メンタルモデルを壊すよくあるミス
ほとんどの React の混乱は構文の問題ではなく、コードが頭の中の単純な物語と一致しなくなったときに起きます。
ミスのパターン(現れる症状)
導出された state を保存してしまう。 fullName を firstName + lastName から state に保存すると、片方だけが変わったときに古い値が表示されます。
エフェクトループ。 エフェクトがデータをフェッチし state をセットし、依存リストがそれを再度走らせる。症状はリクエストの繰り返し、UI の揺れ、または収束しない state。
古いクロージャ。 クリックハンドラが古い値(古いカウンタやフィルタ)を読んでしまう。「クリックしたのに昨日の値を使っている」という症状です。
グローバル state を乱用する。 全ての UI 細部をグローバルストアに入れると、何がどこに属するか分からなくなります。症状は一つを変えただけで三つの画面が意図せず反応することです。
ネストしたオブジェクトのミューテーション。 配列やオブジェクトをその場で変更して UI が更新されないと不思議に思う。症状は「データは変わったのに再レンダーが起きない」です。
具体例: 「検索とソート」パネル。filteredItems を state に保存すると、新しいデータが来たときに items と filteredItems がずれることがあります。代わりに入力(検索テキスト、ソート選択)を保存し、フィルタ済みリストはレンダーで計算してください。
エフェクトは外の世界との同期に使い、基本的な UI 作業のために使わないでください。エフェクトが UI を計算しているなら、それは多くの場合レンダーやイベントハンドラに移すべき仕事です。
チャットでコードを生成したり編集したりすると、これらのミスは大きな塊で変更が来るためもっと早く顕在化します。良い習慣はリクエストを「この値の真の所有者はどこか?」や「計算できるなら保存するべきか?」という形で組み立てることです。
状態が混乱し始めたときのクイックチェックリスト
UI が予測不可能に感じられるとき、それはたいてい「React が多すぎる」のではなく、「state が多すぎる、あるいは間違った場所にあり、本来の役割を超えている」ことが原因です。
次に useState を追加する前に一旦立ち止まり問いかけてください:
- 現状の画面を完全に記述するために最小の値は何か?
- それぞれの値はどこに置けば、必要な部分が読み取れて、不要な部分は影響を受けないか?
- これは本当に新しい情報か、それとも props、URL パラメータ、他の state から計算できるか?
- UI を計算するためにエフェクトを使っていないか?エフェクトは外(ネットワーク、タイマー、ブラウザ API)と同期するためだけか?
小さな例: 検索ボックス、フィルタードロップダウン、リスト。query と filteredItems の両方を state にすると真実が二か所になります。代わりに query と filter を state に持ち、filteredItems はフルリストからレンダーで導出しましょう。
チャットツールで素早く作るときも同じです。速さは良いですが、毎回「state を追加したのか、それとも誤って導出値を state に入れたのか?」と自問してください。導出ならその state を消してレンダーで計算する方が正しいことが多いです。
例: 予測可能性を失わずに UI を素早く反復する
小さなチームが管理 UI を作っています: 注文のテーブル、いくつかのフィルタ、注文を編集するダイアログ。最初の要求は曖昧に聞こえます: 「フィルタと編集ポップアップを追加して」。単純に見えますが、しばしばランダムに散らばった state が増えることになります。
リクエストを状態とイベントに翻訳して具体化してください。「フィルタ」と言われたら state に名前を付けます: query, status, dateRange。「編集ポップアップ」と言われたらイベントに名前を付けます: 「ユーザーが行の Edit をクリックした」。次に各 state の所有者(ページ、テーブル、ダイアログ)と導出可能なもの(フィルタ済みリスト)を決めます。
例としてチャット生成に向いたプロンプト:
OrdersPageがfiltersとselectedOrderIdを所有する。OrdersTableはfiltersによって制御され、onEdit(orderId)を呼ぶ。visibleOrdersはordersとfiltersから導出する。visibleOrdersを state に保存しない。EditOrderDialogはorderとopenを受け取り、保存されたらonSave(updatedOrder)を呼んで閉じる。- データをフェッチするなら一か所に保持し、エフェクトは
filtersを URL と同期するためだけに使い、行のフィルタ計算には使わない。
UI が生成・更新されたら、各 state 値の所有者が一つであるか、導出値が保存されていないか、エフェクトは外部同期だけに使われているか、イベントは props とコールバックで上下に流れているかを短く確認してください。
状態が予測可能なら、反復は安全に感じられます。テーブルのレイアウトを変えたり、新しいフィルタを追加したり、ダイアログのフィールドを調整しても、どの隠れた state が壊れるかを推測する必要がなくなります。
次のステップ: チャットで速く作るが React のモデルを守る
速さはアプリが理由を追えるままであるときにだけ役に立ちます。最も簡単な保護は、これらのメンタルモデルを機能の前に当てはめるチェックリストのように使うことです。
各機能を同じ方法で始めましょう: 必要な state、state を変えるイベント、そして誰が所有するかを書き出す。もし「このコンポーネントがこの state を所有し、これらのイベントがそれを更新する」と言えないなら、散らかった state と驚くべき再レンダリングが起きる可能性が高いです。
チャットで作るときはまず計画モードから始めます。コンポーネント、state の形、遷移を平易な言葉で説明してからコードを求めてください。例: 「フィルタパネルが query state を更新する; 結果リストは query から導出される; 項目を選ぶと selectedId をセットし、閉じるとクリアする」。その文がきれいに読めれば、UI の生成は機械的なステップになります。
Koder.ai (koder.ai) を使って React コードを生成するなら、ワークフローに短い整合性チェックを入れる価値があります: 各 state 値にひとつの明確な所有者、UI は state から導出、エフェクトは同期のためだけ、真実の複製はないこと。
その後は小さなステップで反復してください。state 構造を変えたいとき(例えば複数のブールをひとつの status フィールドに変えるなど)は、まずスナップショットを取り、試してみてメンタルモデルが悪化したらロールバックします。より深いレビューや引き継ぎが必要なときは、ソースコードをエクスポートして状態形状が UI の物語を語り続けているかを確認するのが簡単になります。
よくある質問
React を予測可能にする最もシンプルなメンタルモデルは何ですか?
良い出発点は: UI = f(state, props) です。コンポーネントは DOM を「直接編集する」のではなく、現在のデータに対して画面に何を表示するかを記述するものです。画面が間違っているなら、DOM ではなくその画面を作り出した state/props を調べてください。
何を props にして何を state にすべきかはどう決めますか?
Props は親からの入力です。コンポーネントはそれを読み取り専用として扱うべきです。State はコンポーネントの記憶で、どのコンポーネントが所有するかを決めるものです。値を共有する必要があるなら、それを上に持ち上げ(lift up)て props として渡します。
React が「再レンダー」するとき、ページ全体を作り直しているのですか?
再レンダーとは React が コンポーネント関数を再実行して次の UI 記述を計算することを意味します。必ずしもページ全体が再描画されるわけではありません。React はその後、実際の DOM に最小限の差分だけを適用します。
setState を呼んだ後に状態が「一歩遅れて」見えるのはなぜですか?
状態の更新は スケジュールされたリクエストであって即時代入ではありません。次の値が前の値に依存するなら、アップデータ形式を使ってください:
setCount(c => c + 1)
こうすると複数の更新がキューに入っても正しく動作します。
「導出された state」バグを避けるにはどうすればいいですか?
既存の入力から計算できるものを state に保存しないでください。入力を保存し、残りはレンダー時に導出します。
例:
- 保存する:
items,filter - 導出する:
visibleItems = items.filter(...)
これで値がずれるのを防げます。
useEffect はいつ使い、いつ避けるべきですか?
エフェクトは React が制御しないものと同期するために使います:ネットワーク、サブスクリプション、タイマー、ブラウザ API、あるいは命令的な DOM 操作など。
UI 値を状態から計算するためだけにエフェクトを使うのは避けてください。レンダー中に計算するか、本当に高価なら useMemo を使います。
useEffect の依存配列はどう考えればいいですか?
依存配列は “これらの値が変わったら再同期する” という トリガー一覧 と考えてください。パフォーマンスのトリックでも、警告を黙らせる場所でもありません。
例えば userId が変わったらユーザー詳細をフェッチするなら、userId は依存配列に入れるべきです。エフェクトが token を使うならそれも含めないと古いトークンでフェッチしてしまいます。
いつ state を上に持ち上げるべきですか?
2 つの UI 部分が常に同じ値を示す必要があるなら、最も近い共通の親に state を置き、値を渡し、更新はコールバックで受け取るようにします。
短いテスト: 同じ値を二か所に複製して同期させるためにエフェクトを書いているなら、その state は単一の所有者にするべきです。
なぜイベントハンドラが古い値を使ってしまうことがあるのですか?
ハンドラが以前のレンダーで捕まえた古い値を使っていることが原因です。一般的な対処法:
- アップデータを使う:
setX(prev => ...) - 変化する値は普通の変数ではなく state/refs に置く
- エフェクトの依存に必要な値を含める
クリックが「昨日の値」を使うなら、古いクロージャを疑ってください。
チャット経由で素早く React UI を作るときに、状態を汚さないにはどうしたらいいですか?
小さな計画から始めて、コンポーネント、state の所有者、イベントを決めてください。コードは小さなパッチ(ステートを 1 つ追加、ハンドラを 1 つ追加、値をレンダーで導出)として生成する方が安全です。
Koder.ai のようなチャットビルダーを使う場合は、各 state に一つのオーナー、導出値はレンダーで計算、エフェクトは同期用途に限定するよう指示してください。これで生成コードが React のメンタルモデルに沿います。