Next.jsで学ぶReact講座(完全初心者向け・30日) | 第4章:実用的React – 状態設計の考え方

React Next.js
スポンサーリンク

状態設計の全体イメージ

React / Next.js を「とりあえず動かす」ところから一歩進むと、必ずぶつかるのが「状態(state)をどこに持つか問題」です。
コンポーネントを分割すればするほど、

どこに state を置くべきか
どこまで props で渡すべきか

が分からなくなってきます。

ここを雑にすると、いわゆる「props 地獄」になって、
コードを触るのが一気にしんどくなります。

逆に、状態設計の考え方を一度ちゃんと整理しておくと、
アプリが大きくなっても「シンプルさ」を保ちやすくなります。


stateの持ち場所の基本ルール

「その状態を誰が使うか」で決める

state の持ち場所を決めるときの一番シンプルな基準は、

その状態を「どのコンポーネントたち」が使うのか

です。

例として、「講座一覧+フィルタ」の画面を考えます。

講座一覧を表示する LessonList
フィルタ条件(レベルなど)を選ぶ LessonFilter

このとき、「選択中のレベル」という state はどこに置くべきか。

LessonFilter だけが使うなら、LessonFilter の中に持てばいい。
でも、「選択中のレベルに応じて LessonList の中身も変えたい」なら、
LessonFilterLessonList の「共通の親」に state を置く必要があります。

これがよく言われる

「状態は、それを必要とするコンポーネントたちの“最も近い共通の親”に置く」

というルールです。

小さな例でイメージする

例えば、こんな構造だとします。

function Page() {
  return (
    <main>
      <LessonFilter />
      <LessonList />
    </main>
  );
}
TSX

このとき、「選択中のレベル」を LessonFilterLessonList の両方で使いたいなら、
Page に state を置いて、両方に props で渡すのが素直です。

function Page() {
  const [level, setLevel] = useState<"all" | "beginner" | "intermediate">("all");

  return (
    <main>
      <LessonFilter level={level} onChangeLevel={setLevel} />
      <LessonList level={level} />
    </main>
  );
}
TSX

このように、

「誰がその状態を読むか・変えるか」
「その人たちの一番近い親はどこか」

を考えるのが、state の持ち場所を決める基本です。


props地獄の回避

「とりあえず全部親に置く」は危険

初心者がやりがちなのは、

とりあえず全部一番上のコンポーネントに state を置く
そこから孫・ひ孫コンポーネントまで props で延々と渡す

というパターンです。

例えば、こんな感じ。

AppselectedLesson を持っている
AppLayoutSidebarLessonListLessonItem
と 5 段階くらい props を渡していく

こうなると、

どこで state が変わっているのか分かりにくい
中間のコンポーネントが「ただ props を受け取って流すだけ」になる

という「props 地獄」に陥ります。

「本当にそのコンポーネントで必要か?」を問い直す

props 地獄を避けるための最初の一歩は、

その props は、本当にそのコンポーネントで必要か?

を一つひとつ問い直すことです。

中間のコンポーネントが「受け取ってそのまま子に渡すだけ」になっているなら、
設計を見直すサインです。

例えば、LayoutselectedLesson を何も使っていないなら、
Layout を経由せずに、Page から LessonList に直接渡す構造に変えられないかを考えます。

コンポーネント分割の単位を見直す

props 地獄の多くは、「分割の仕方」が原因です。

例えば、こういう分割は危険です。

Page
PageHeader
PageBody
PageBodyLeft
PageBodyLeftInner
LessonList

このように「意味の薄いラッパーコンポーネント」が増えると、
state を渡すためだけの props がどんどん増えます。

代わりに、

Page
LessonFilter
LessonList
LessonItem

のように、「役割がはっきりしている単位」で分割すると、
state の流れもシンプルになります。


初心者がやりがちな設計

1. なんでもかんでも state にする

例えば、こんなコードです。

const [title, setTitle] = useState("Next.js 入門");
TSX

この title が一切変わらないのであれば、
state にする必要はありません。
ただの定数で十分です。

const title = "Next.js 入門";
TSX

state は「変わる可能性があるもの」にだけ使うべきです。
なんでもかんでも state にすると、

無駄な再レンダリングが増える
「どこで変わるのか」を追うのが大変になる

というデメリットが出てきます。

2. 子コンポーネントの中で「親が持つべき状態」を持ってしまう

例えば、「モーダルの開閉状態」を考えます。

Page にボタンがあり、Modal コンポーネントを表示したい。
でも、Modal の中で isOpen を持ってしまうと、

親から「開いて」と指示できない
親が「閉じて」と指示できない

という状態になります。

開閉を制御したいのは親側なので、
isOpen は親が持ち、Modal は「開いているかどうか」を props で受け取るべきです。

function Page() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>モーダルを開く</button>
      <Modal isOpen={isOpen} onClose={() => setIsOpen(false)} />
    </>
  );
}
TSX

「誰がその状態をコントロールしたいのか」を考えると、
state の持ち場所が見えやすくなります。

3. 「とりあえずコンテキスト or グローバル」に逃げる

props 地獄がつらくなると、
いきなり Context やグローバル状態管理(Redux など)に逃げたくなります。

でも、初心者のうちから何でもかんでもグローバルにすると、

どこで状態が変わっているか分かりにくい
コンポーネントの再利用性が下がる

という別の問題が出てきます。

まずは、

ローカル state
親子間の props

だけでどこまで設計できるかを試してみるのがおすすめです。
それでも本当に辛くなってから、Context などを検討するほうが健全です。


シンプルに保つ方法

「状態の種類」を意識して整理する

状態設計をシンプルにするコツは、
「状態の種類」を意識して分けて考えることです。

例えば、こんな分類があります。

サーバーから取ってくるデータ(API レスポンス)
ユーザーの一時的な入力値(フォーム)
UI の状態(モーダルの開閉、タブの選択、ローディング中かどうか)

これらを全部ごちゃ混ぜにして一つのコンポーネントに詰め込むと、
一気にカオスになります。

逆に、

データ取得はこのコンポーネント
フォーム入力はこのコンポーネント
UI 状態はこのコンポーネント

というふうに、役割ごとに分けていくと、
state の数が多少多くても、頭の中では整理しやすくなります。

「derived state(導出できる状態)」を持たない

もう一つ大事な考え方が、

「他の state から計算で求められるものは、state にしない」

です。

例えば、

const [items, setItems] = useState<Item[]>([]);
const [count, setCount] = useState(0);
TSX

count が「items の長さ」なら、
わざわざ state に持つ必要はありません。

const count = items.length;
TSX

このように、「計算で出せるもの」を state にしてしまうと、

片方だけ更新されてズレる
どちらが正しいのか分からなくなる

というバグの温床になります。

state は「本当に保存しておく必要がある最小限」に絞る。
それ以外は「レンダリング時に計算する」。
これだけで、状態設計はかなりシンプルになります。

「一画面に持つ state の数」を意識する

感覚的な話ですが、

一つのコンポーネントに 10 個も 20 個も state がある

という状態になってきたら、
「分割のタイミングかも」と疑ってみてください。

例えば、こんな感じです。

検索条件の state が 5 個
ローディング・エラーの state が 2 個
モーダルの開閉が 3 個

これを全部 1 コンポーネントで持つのではなく、

検索条件を扱うコンポーネント
結果一覧を表示するコンポーネント
モーダルをまとめるコンポーネント

に分けて、それぞれが「自分に関係する state だけ」を持つようにすると、
頭の中の負荷が一気に下がります。


まとめと、状態設計を鍛えるための視点

ここまでのポイントを整理すると、

state の持ち場所は、「その状態を必要とするコンポーネントたちの最も近い共通の親」に置く、というのが基本ルール。
props 地獄は、「意味の薄いラッパーコンポーネントが多い」「なんでも親に state を置く」ことで起きるので、分割の単位と props の流れを見直すことが大事。
初心者がやりがちなのは、「変わらないものまで state にする」「子が持つべきでない状態を子に持たせる」「すぐグローバルに逃げる」などで、ここを意識して避けるだけで設計はかなり良くなる。
シンプルに保つには、「状態の種類を分ける」「計算で出せるものは state にしない」「一コンポーネントの state 数が増えすぎたら分割を検討する」といった視点が効いてくる。

もし今、自分のコードで「どこに state を置くか迷っている画面」があれば、
その画面の構造をざっくり教えてくれれば、一緒に「どこに何を置くか」を具体的に整理してみよう。
状態設計は、実際の画面を題材にすると一気に理解が深まります。

タイトルとURLをコピーしました