おい、調子はどうだい?
最近、コードレビューをしていて「あー、またやってるな……」と頭を抱えたくなる瞬間があるんだ。
何かと言うと、新しく実装した機能のモーダルの開閉フラグや、フォームの一時的な入力値まで、ぜーんぶReduxなりZustandなり、あるいは親の親のそのまた親の`useState`にぶち込んでいるコードを見たときだ。
「とりあえずグローバルに持たせとけばどこからでも触れるし楽っしょ!」というノリで設計されたコードベースは、半年後の自分やチームメンバーにとって、地雷原を目隠しで歩くような苦行に変わる。
今日はな、そんな不毛な状態管理の泥沼から抜け出すための最強の共通認識、「状態の局所化(State Colocation)」について、俺の現場の経験を交えて徹底的に解説してやる。心して聞いてくれ。
—
なぜ「何でもかんでもグローバル」は悪手なのか?
中級に差し掛かったエンジニアがよく陥る罠がこれだ。「どこからでも触れる状態=正義」だと思い込んでしまうこと。
ブラウザの裏側の話をしよう。Reactが状態(State)をどう扱っているか知っているか?
Reactは、コンポーネントのツリー構造(Fiberツリー)を上から順番に舐めていき、どのフックが何番目に呼ばれたかをインデックスで管理している。つまり、状態は「そのコンポーネントがツリーのどこに存在するか」に強く依存しているんだ。
それを無視して、UIのほんの一部分でしか使わないトグル状態や入力値を、はるか上の親やグローバルストアに引き上げてしまったらどうなるか?
1. 無駄な再レンダリング(Rerender)の嵐:
グローバルストアや上位の親が更新されるたびに、本来関係のない子コンポーネントまで巻き添えを食って再評価される。ブラウザのメインスレッドは悲鳴を上げ、ユーザーの操作に対する「カクつき」として現れる。
2. 結合度の爆発:
「このコンポーネントを別のページに使い回したい」と思ったとき、親が抱えている謎の依存関係やグローバルストアのモックをごっそり持ってこないと動かない。再利用性(Reusability)の完全な死だ。
3. 認知負荷の増大:
「この値はいったいどこで書き換えられているんだ?」と追うために、何ファイルもジャンプさせられる。デバッグのたびにタイムロスが発生する。
これらを綺麗に解決する指針が、「State Colocation(状態の局所化)」だ。
—
状態の局所化(State Colocation)の原則とは?
ルールは極めてシンプルだ。
> 「状態は、それを必要とする最も近いコンポーネントの近くに配置せよ。」
ただそれだけ。もしその状態を必要とするのがたった1つの子コンポーネントだけなら、親にその状態を持たせるな。その子コンポーネントの中に閉じ込めろ。複数の子コンポーネントで共有する必要がある場合のみ、共通の最も近い親まで引き上げろ。それ以上上に持っていくな。
百聞は一見に如かずだ。実際のコードで「アンチパターン」と「ベストプラクティス」を見比べてみよう。
—
実務で使える!コードで見るリファクタリング
ここでは、「ユーザー一覧」を表示しつつ、各ユーザーカードに「詳細アコーディオンの開閉状態」と「お気に入りボタンのローディング状態」を持つUIを想定する。
❌ やりがちなアンチパターン(状態の過剰な引き上げ)
すべての状態を親(Page)が管理し、子にpropsで無理やり配っている例だ。
import React, { useState } from ‘react’;
// アンチパターン:親がすべてを知りたがる設計
export const UserManagementPage = () => {
// すべての状態が親に集中している
const [openUserId, setOpenUserId] = useState
const [loadingUserId, setLoadingUserId] = useState
const [users] = useState([
{ id: ‘1’, name: ‘Taro Yamada’, bio: ‘Frontend Architect’ },
{ id: ‘2’, name: ‘Hanako Suzuki’, bio: ‘UI/UX Designer’ },
]);
const handleToggle = (id: string) => {
setOpenUserId(prev => (prev === id ? null : id));
};
const handleFavorite = async (id: string) => {
setLoadingUserId(id);
// 何らかの非同期処理のつもり
await new Promise((resolve) => setTimeout(resolve, 1000));
setLoadingUserId(null);
alert(`User ${id} favorited!`);
};
return (
ユーザー管理
{users.map((user) => {
const isOpen = openUserId === user.id;
const isLoading = loadingUserId === user.id;
return (
{user.name}
{/ 状態とハンドラーをわざわざpropsでバケツリレーしている /}
{isOpen &&
{user.bio}
}
);
})}
);
};
何がダメか?
ユーザーAの「お気に入りボタン」を押してローディング状態(`loadingUserId`)が変化しただけで、関係のないユーザーBのカードも含めて親コンポーネント(`UserManagementPage`)全体が再レンダリングされる。リストの規模が大きくなったら一発でパフォーマンスが死ぬ。
—
⭕ 正しいベストプラクティス(状態の局所化)
各カードのコンポーネントを独立させ、状態を閉じ込める。
import React, { useState } from ‘react’;
// 各ユーザーのデータ型
type User = {
id: string;
name: string;
bio: string;
};
// 【改善ポイント1】個別のカードコンポーネントにUIと状態を閉じ込める(Colocation)
const UserCard: React.FC<{ user: User }> = ({ user }) => {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleFavorite = async () => {
setIsLoading(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsLoading(false);
alert(`User ${user.id} favorited!`);
};
return (
{user.name}
{/ このコンポーネント内のuseStateが更新されても、親や他のカードは再レンダリングされない /}
{isOpen &&
{user.bio}
}
);
};
// 親はリストのデータとレンダリングにのみ責任を持つ
export const UserManagementPage = () => {
const [users] = useState
{ id: ‘1’, name: ‘Taro Yamada’, bio: ‘Frontend Architect’ },
{ id: ‘2’, name: ‘Hanako Suzuki’, bio: ‘UI/UX Designer’ },
]);
return (
ユーザー管理
{users.map((user) => (
))}
);
};
何が素晴らしいのか?
1. `UserCard` が完全に独立した。このコンポーネントは、他のページや別のプロジェクトに持っていってもそのまま動く(ポータビリティの向上)。
2. あるカードの開閉やローディングを行っても、そのカードだけがピンポイントで再レンダリングされる。Reactのパフォーマンス最適化の基本中の基本だ。
3. コードの見通しが劇的に良くなり、「どこが何を管理しているか」が一目でわかる。
—
シニアからの実践的なアドバイス
現場で設計に迷ったときは、自分にこう問いかけてみてほしい。
> 「この状態を消したら、アプリの他のどの部分が困るか?」
もし答えが「このコンポーネント自身だけ」なら、迷わずそのコンポーネントの中に`useState`を置くんだ。親に引き上げるのは、「本当に離れた場所にある複数の兄弟コンポーネントが、そのデータを共有しなければならない時」の最後の手段にとっておきなさい。
最初からグローバルストアに逃げたり、何でも親に状態を持たせるのは、設計をサボっているのと一緒だ。
局所化を制する者は、Reactのパフォーマンスと保守性を制す。今日のコードから、ぜひ意識してみてくれ。さて、次のタスクに取り掛かろうか!

コメント