お疲れ。最近、君が書いたコンポーネントのPRを見たんだけどさ……子コンポーネント同士でデータを共有したいがために、無理やりReduxを持ち出したり、無駄にContext APIを乱用して再レンダリングの嵐を起こしていた箇所があったよね。
気持ちは分からなくもない。複雑なアプリを作っていると、「状態はグローバルに管理すべきだ」という強迫観念に囚われがちだからな。だけど、ちょっと待て。Reactの本質はもっとシンプルだ。
今回は、Reactのデータフローの基本にして、中級から一歩抜け出すための最大の武器である「状態の持ち上げ(Lifting State Up)」について話をしよう。実務で明日から即座に使える設計の極意を叩き込むから、コーヒーでも飲みながら聞いてくれ。
—
なぜ「状態の持ち上げ」が必要なのか?
Reactのデータフローの鉄則、それは「単方向データフロー(One-way data flow)」だ。データは常に親から子へ、プロパティ(`props`)という片道切符で流れていく。兄弟コンポーネント同士が直接データをやり取りすることは、Reactの哲学において御法度なんだよ。
例えば、よくあるこんなUIを想像してほしい。
- 左側に「入力フォーム(子A)」がある。
- 右側に「リアルタイムプレビュー(子B)」がある。
子Aに入力された値を、子Bに即座に反映させたい。ここで初心者がやりがちなのが、「子Aの中に状態を持ち、なんとかして兄弟である子Bにそれを渡そうとする」という泥沼だ。無理だ。構造上、兄弟間で直接の授受はできない。
じゃあどうするか?答えは単純明快。「二人の共通の親に、状態の管理責任をごっそり引き受けてもらう(=持ち上げる)」これだけだ。
—
ブラウザの裏側で何が起きているか?(Reactのレンダリングメカニズム)
ここで少し、Reactが内部でどう動いているかという話をしよう。これを理解していないと、「とりあえず持ち上げたら動いたけど、なぜかアプリが重くなった」という罠にハマる。
`useState`で状態を親に持ち上げると、その状態が更新された瞬間、親コンポーネントが再評価(Re-render)される。 そして、親が再評価されると、その配下にある子コンポーネントたちもデフォルトではれれなく再評価の対象になる。
「え、じゃあパフォーマンス悪くなるじゃん?」と思ったそこの君、鋭い。
だからこそ、実務では「どのレベルまで状態を持ち上げるべきか」の境界線を見極める必要がある。無駄に最上位のAppコンポーネントまで状態を持ち上げたら、アプリ全体が再レンダリングされてパフォーマンスが死ぬ。
原則はこうだ:「その状態を必要とする最小限の共通の祖先(Closest Common Ancestor)」にのみ持ち上げること。これがアーキテクチャの美学であり、プロの仕事だ。
—
実践!コピペで動くクリーンなサンプルコード
口ばかりじゃ説得力がないからな。実務を想定した、ちょっとした「フィルター付きアイテムリスト」のコードを書いた。
親が状態を持ち、子A(入力フォーム)と子B(リスト表示)を統制する美しいパターンだ。そのままエディタに貼って動かしてくれ。
import React, { useState } from ‘react’;
// ==========================================
// 1. 子コンポーネントA: 検索フィルター入力
// ==========================================
type SearchFilterProps = {
keyword: string;
onKeywordChange: (newKeyword: string) => void;
};
// 親から渡された状態と、それを更新する関数(セッター)だけを受け取る
const SearchFilter: React.FC
console.log(‘SearchFilter が再レンダリングされました’);
return (
onKeywordChange(e.target.value)} // 入力値を親へ通知
placeholder=”キーワードを入力…”
style={{ padding: ‘8px’, width: ‘100%’, maxWidth: ‘300px’ }}
/>
);
};
// ==========================================
// 2. 子コンポーネントB: リスト表示
// ==========================================
type ItemListProps = {
items: string[];
};
const ItemList: React.FC
console.log(‘ItemList が再レンダリングされました’);
return (
-
{items.length > 0 ? (
- {item}
items.map((item, index) =>
)
) : (
一致するアイテムはありません。
)}
);
};
// ==========================================
// 3. 親コンポーネント: 状態の持ち上げ先(Lifting State Up)
// ==========================================
const MOCK_ITEMS = [
‘React 基礎から実践まで’,
‘TypeScript 型安全の極意’,
‘Next.js App Routerのアーキテクチャ’,
‘Tailwind CSS 爆速スタイリング’,
‘State Managementの黄昏’,
];
export const ParentDashboard: React.FC = () => {
// ★ここに状態を持ち上げる!子Aと子Bの「共通の親」だからこそ、ここで一元管理できる。
const [keyword, setKeyword] = useState
// 状態(keyword)をもとに、表示用のデータを親の責任でフィルタリングする
const filteredItems = MOCK_ITEMS.filter((item) =>
item.toLowerCase().includes(keyword.toLowerCase())
);
return (
ダッシュボード(状態の持ち上げデモ)
{/ 状態と、それを変更するトリガーをpropsとして子Aに渡す /}
{/ フィルタリング済みのデータを子Bに渡す /}
);
};
export default ParentDashboard;
このコードの美しいポイント
1. 単方向データフローの遵守:
`SearchFilter`(子A)は自身で状態を持たず、入力値が変わるたびに親の`onKeywordChange`(つまり`setKeyword`)を叩いている。データは親から子へ(`keyword`)、アクションは子から親へ(`onKeywordChange`)という綺麗な循環が生まれている。
2. 派生状態(Derived State)の計算:
`filteredItems`は新しい`useState`として保持していない。親のレンダリングのたびに`keyword`から計算されている。余計なステートを増やさないことで、「状態の同期ズレ(バグの温床)」を完全にシャットアウトしているんだ。
—
シニアからの実務アドバイス
「状態の持ち上げ」は非常に強力だが、一つだけ実務で気をつけるべきアンチパターンを伝えておく。
それは、「何でもかんでも親に持ち上げすぎて、巨大なモンスターコンポーネント(God Component)を作ってしまうこと」だ。
すべての入力値、モーダルの開閉状態、タブの切り替え……これらを一つの親に集約させると、親が再レンダリングされるたびにアプリ全体がプチフリーズするようなクソコードが完成する。
もし状態の持ち上げによってプロパティのバケツリレー(Prop Drilling)が3階層以上に及ぶようになったり、親が肥大化し始めたら、それは「Context APIの導入」や「カスタムフックへのロジック切り出し」を検討するシグナルだ。
まずは適切な粒度で「状態の持ち上げ」をマスターすること。それができれば、君の書くReactコードの品質は一段も二段も跳ね上がるはずだ。
さて、理論はここまでだ。実際のコードに落とし込んで、手を動かしてみようか。何か詰まったらいつでも声をかけてくれ。

コメント