モダンJavaScriptの基礎から始める挫折しないためのReact入門 60日コース | フォーム・コンポーネント設計 - DAY 38:Stateはどこに置くか

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 38:Stateはどこに置くか 前半 ― 「状態の居場所」を意識して設計する
  2. 親のStateと子のProps ― 「誰が持って、誰が受け取るか」
    1. 親コンポーネントがStateを持ち、子コンポーネントがPropsとして受け取る
    2. なぜ親がStateを持つのか
  3. 子のProps ― 「親から渡されたものを使うだけ」
    1. Propsは「読み取り専用の入力」として扱う
    2. 子コンポーネントが「イベント」を親に知らせるパターン
  4. State Lifting(ステートリフティング)とは何か
    1. 「バラバラに持っていたStateを、より上のコンポーネントに集める」こと
    2. 簡易ユーザー登録アプリでのState Liftingのイメージ
  5. 共通の親 ― 「みんなが見たい状態は、共通の親に置く」
    1. なぜ「共通の親」が重要なのか
    2. 共通の親にStateを置く具体例
  6. DAY 38 前半のまとめ ― 「Stateの居場所」を意識して設計する第一歩
  7. DAY 38:Stateはどこに置くか 中盤 ― 実際のフォームで「親のState」と「子のProps」を体に落とし込む
  8. 親のStateを「アプリの真ん中」に置く感覚をつかむ
    1. まずは「親が全部持っている」形を確認する
  9. 子のPropsは「親から渡された入力+イベントフック」として扱う
    1. UserFormは「入力+バリデーション+親への通知」に集中させる
    2. UserListは「表示+削除イベントの通知」に集中させる
  10. State Liftingを「実際に起きていること」として理解する
    1. もしStateを子に置いていたらどうなるかを考える
    2. State Liftingの流れを言葉で整理する
  11. 共通の親を「状態のハブ」として意識する
    1. 共通の親は「みんなが見たい状態の持ち主」
  12. DAY 38 中盤のまとめ ― 「親のState/子のProps/State Lifting/共通の親」をコードで腑に落とす
  13. DAY 38:Stateはどこに置くか 後半 ― 「自分で判断できる状態設計」の感覚を育てる
  14. 親のStateを「アプリの真ん中に置く」という発想
    1. どのStateが「アプリ全体の意味」を持つかを考える
    2. 「フォームだけで完結するState」と「アプリ全体で意味を持つState」を分ける
  15. 子のPropsを「親とのインターフェース」として意識する
    1. Propsは「親と子の約束ごと」
  16. State Liftingを「設計の判断プロセス」として理解する
    1. State Liftingは「必要な場所を見てから決める」
    2. State Liftingの「前」と「後」を比べてみる
  17. 共通の親を「状態のハブ」として設計する
    1. 共通の親は「みんなが見たい状態の持ち主」
  18. DAY 38 後半版「状態設計チェックリスト」
    1. 親のStateチェック
    2. 子のPropsチェック
    3. State Liftingチェック
    4. 共通の親チェック
  19. DAY 38 後半のまとめ ― 「Stateはどこに置くべきか」を自分で決められるようになる

DAY 38:Stateはどこに置くか 前半 ― 「状態の居場所」を意識して設計する

DAY 38では、フォーム・コンポーネント設計の中でもとても重要なテーマである 「Stateはどこに置くか」について学んでいきます。

学習項目は次の4つです。

  • 親のState
  • 子のProps
  • State Lifting(ステートリフティング)
  • 共通の親

これらは、Reactで少し本格的なアプリを作り始めると必ずぶつかる考え方です。 プログラミング初心者向けに、例題とコードを交えながら、ステップバイステップでかみ砕いて説明していきます。

親のStateと子のProps ― 「誰が持って、誰が受け取るか」

親コンポーネントがStateを持ち、子コンポーネントがPropsとして受け取る

Reactでは、

「状態(State)はどこかのコンポーネントが持ち、その値を他のコンポーネントに渡すときはPropsを使う」

というのが基本の流れです。

よくあるパターンは、

  • 親コンポーネントがStateを持つ
  • 子コンポーネントがそのStateをPropsとして受け取る

という形です。

簡単な例を見てみます。

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>カウンター</h2>
      <Child count={count} />
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

function Child({ count }) {
  return <p>現在のカウント:{count}</p>;
}
JSX

ここでの役割は次のようになります。

  • Parent
    • count というStateを持つ
    • ボタンで count を更新する
    • countChild に渡す
  • Child
    • count をPropsとして受け取り、表示するだけ

重要なポイント:

「状態を持つのはParent、表示するのはChild」という役割分担になっています。 子コンポーネントは「状態を持たず、Propsを受け取って表示するだけ」という形が基本です。

なぜ親がStateを持つのか

親コンポーネントがStateを持つ理由は、

  • 親が「全体の状況」を把握しやすくなる
  • 複数の子コンポーネントに同じStateを渡せる
  • 子コンポーネントは「表示に集中」できる

からです。

例えば、フォームと一覧表示を組み合わせる場合、

  • 親コンポーネントが「ユーザー一覧のState」を持つ
  • フォームコンポーネントは「新しいユーザーを追加するための入力」だけを担当する
  • 一覧コンポーネントは「渡されたユーザー一覧を表示する」だけを担当する

という構造にすると、全体がとても整理されます。

子のProps ― 「親から渡されたものを使うだけ」

Propsは「読み取り専用の入力」として扱う

子コンポーネントが受け取るPropsは、

「親から渡された読み取り専用の入力」

だと考えると分かりやすいです。

先ほどの例では、Child は次のように定義されていました。

function Child({ count }) {
  return <p>現在のカウント:{count}</p>;
}
JSX

ここでの count は、

  • 親から渡された値
  • 子の中では「表示するだけ」
  • 子の中で count を直接書き換えることはしない

という扱いになります。

重要なポイント:

Propsは「外から渡された値」であり、 子コンポーネントはそれを勝手に変更しないのが基本ルールです。

子コンポーネントが「イベント」を親に知らせるパターン

子コンポーネントは、Propsとして「値」だけでなく「関数」を受け取ることもあります。

例えば、ボタンを押したときに親に知らせたい場合は、次のように書きます。

function Parent() {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    setCount((prev) => prev + 1);
  };

  return (
    <div>
      <h2>カウンター</h2>
      <Child count={count} onIncrement={handleIncrement} />
    </div>
  );
}

function Child({ count, onIncrement }) {
  return (
    <div>
      <p>現在のカウント:{count}</p>
      <button onClick={onIncrement}>+1</button>
    </div>
  );
}
JSX

ここでの流れは、

  • 子コンポーネントは onIncrement という関数をPropsとして受け取る
  • ボタンがクリックされたら onIncrement() を呼ぶ
  • 実際にStateを更新するのは親コンポーネント

という形になります。

重要なポイント:

子コンポーネントは「イベントを親に知らせる」役割を持ち、 実際のState更新は親が担当する、という分担が基本です。

State Lifting(ステートリフティング)とは何か

「バラバラに持っていたStateを、より上のコンポーネントに集める」こと

State Lifting(ステートリフティング)とは、

「複数のコンポーネントで共有したい状態を、より上の親コンポーネントに移動させること」

を指します。

例えば、次のような状況を考えてみます。

  • FormAFormB という2つのフォームコンポーネントがある
  • それぞれが自分の中に inputValue のStateを持っている
  • 親コンポーネントで「両方のフォームの入力内容をまとめて表示したい」

このとき、フォームごとにStateを持っていると、 親コンポーネントからはそれぞれの入力内容が見えません。

そこで、

  • inputValueAinputValueB を親コンポーネントに移動する
  • FormAFormB は、親から渡された値と更新関数を使う

という形にするのが、State Liftingです。

簡易ユーザー登録アプリでのState Liftingのイメージ

簡易ユーザー登録アプリを例にすると、次のようなState Liftingが起きています。

  • ユーザー一覧(users)は親コンポーネントが持つ
  • フォームコンポーネントは、登録されたユーザーを親に渡すだけ
  • 一覧コンポーネントは、親から渡された users を表示するだけ

もし、フォームコンポーネントが自分の中に users を持っていたら、

  • 一覧コンポーネントからはその users が見えない
  • 親コンポーネントも users を知らない

という状態になり、アプリ全体の状態管理が難しくなります。

そこで、

「ユーザー一覧という状態は、フォームだけでなく一覧表示にも関係する。 だから、両方のコンポーネントの共通の親にStateを置こう。」

という判断をするのが、State Liftingです。

共通の親 ― 「みんなが見たい状態は、共通の親に置く」

なぜ「共通の親」が重要なのか

State Liftingの話とつながりますが、

「複数のコンポーネントで共有したい状態は、そのコンポーネントたちの共通の親に置く」

というのがReactの基本的な設計方針です。

例えば、

  • UserFormUserList の両方が「ユーザー一覧」を必要としている
  • UserForm は「新しいユーザーを追加する」
  • UserList は「現在のユーザー一覧を表示する」

このとき、「ユーザー一覧」はどこに置くべきでしょうか?

答えは、

UserFormUserList の共通の親である UserPage に置く

です。

共通の親にStateを置く具体例

function UserPage() {
  const [users, setUsers] = useState([]);

  const handleAddUser = (user) => {
    setUsers((prevUsers) => [...prevUsers, user]);
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー登録ページ</h2>

      <UserForm onAddUser={handleAddUser} />

      <UserList users={users} onDeleteUser={handleDeleteUser} />
    </div>
  );
}
JSX

ここでの構造は、

  • usersUserPage が持つ(共通の親)
  • UserFormonAddUser を通じて新しいユーザーを追加する
  • UserListusers を受け取って表示し、onDeleteUser を通じて削除を親に知らせる

という形になっています。

重要なポイント:

「複数のコンポーネントが同じ状態を必要としているなら、その状態は共通の親に置く」 これが、State Liftingと共通の親の考え方の核心です。

DAY 38 前半のまとめ ― 「Stateの居場所」を意識して設計する第一歩

DAY 38前半で身につけてほしいポイントは次の通りです。

  • 親コンポーネントがStateを持ち、子コンポーネントはPropsとしてその値を受け取るのが基本であること。
  • Propsは「読み取り専用の入力」であり、子コンポーネントはそれを勝手に変更しないこと。
  • State Lifting(ステートリフティング)は、「複数のコンポーネントで共有したい状態を、より上の親コンポーネントに移動させること」であること。
  • 複数のコンポーネントが同じ状態を必要としている場合、その状態は「共通の親」に置くのがReactの基本方針であること。

この「Stateはどこに置くべきか」という感覚が少しずつ育ってくると、 フォームや一覧表示だけでなく、 より複雑な画面でも落ち着いて状態管理を設計できるようになっていきます。


DAY 38:Stateはどこに置くか 中盤 ― 実際のフォームで「親のState」と「子のProps」を体に落とし込む

DAY 38の中盤では、前半で学んだ

  • 親のState
  • 子のProps
  • State Lifting
  • 共通の親

を、実際のフォーム構成の中でどう使うかに焦点を当てて解説していきます。 ここでは、簡易ユーザー登録アプリを題材にしながら、「Stateはどこに置くべきか」をステップバイステップで考えていきます。

親のStateを「アプリの真ん中」に置く感覚をつかむ

まずは「親が全部持っている」形を確認する

簡易ユーザー登録アプリを、親コンポーネントがすべてのStateを持つ形で書いてみます。

import { useState } from "react";

function UserPage() {
  // ユーザー一覧(アプリ全体で共有したい状態)
  const [users, setUsers] = useState([]);

  // 新規ユーザー追加
  const handleAddUser = (user) => {
    setUsers((prevUsers) => [...prevUsers, user]);
  };

  // ユーザー削除
  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー登録ページ</h2>

      {/* 入力フォーム(子)には「追加用の関数」だけ渡す */}
      <UserForm onAddUser={handleAddUser} />

      {/* 一覧表示(子)には「一覧」と「削除用の関数」を渡す */}
      <UserList users={users} onDeleteUser={handleDeleteUser} />
    </div>
  );
}

export default UserPage;
JSX

ここでのポイントは、

  • ユーザー一覧(users)は親が持つ
  • 親が「追加」「削除」のロジックを担当する
  • 子コンポーネントは「表示」と「入力」に集中する

という役割分担になっていることです。

重要な感覚:

「アプリ全体で意味を持つ状態は、親に置く」 これが、親のStateの基本的な考え方です。

子のPropsは「親から渡された入力+イベントフック」として扱う

UserFormは「入力+バリデーション+親への通知」に集中させる

次に、UserForm を見てみます。 ここでは、フォーム内部の入力値はフォーム自身が持ち、 登録されたユーザーは親に渡す、という構造にします。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;

function UserForm({ onAddUser }) {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");

  const validateName = (name) => {
    if (name.trim() === "") {
      return "名前は必須です。";
    }
    if (name.length > MAX_NAME_LENGTH) {
      return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
    }
    return "";
  };

  const validateEmail = (email) => {
    if (email.trim() === "") {
      return "メールアドレスは必須です。";
    }
    if (!email.includes("@")) {
      return "メールアドレスの形式が正しくありません。";
    }
    return "";
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    setNameError("");
    setEmailError("");

    const nameValidationResult = validateName(name);
    const emailValidationResult = validateEmail(email);

    const hasError =
      nameValidationResult !== "" || emailValidationResult !== "";

    if (nameValidationResult !== "") {
      setNameError(nameValidationResult);
    }

    if (emailValidationResult !== "") {
      setEmailError(emailValidationResult);
    }

    if (hasError) {
      return;
    }

    const newUser = {
      id: Date.now(),
      name,
      email,
    };

    // ★ 親から渡された関数を呼び出して「新しいユーザーができたよ」と知らせる
    onAddUser(newUser);

    setName("");
    setEmail("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>
          名前:
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="例:山田太郎"
          />
        </label>
        {nameError && (
          <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
        )}
      </div>

      <div style={{ marginTop: "8px" }}>
        <label>
          メール:
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="例:taro@example.com"
          />
        </label>
        {emailError && (
          <p style={{ color: "red", marginTop: "4px" }}>{emailError}</p>
        )}
      </div>

      <div style={{ marginTop: "12px" }}>
        <button type="submit">登録</button>
      </div>
    </form>
  );
}

export default UserForm;
JSX

ここでのPropsの役割は、

  • onAddUser: 親から渡された「ユーザー追加用の関数」 フォームは、バリデーションを通過したユーザーをこの関数に渡すだけ

です。

重要なポイント:

子コンポーネントは「親から渡された関数を呼ぶことで、イベントを親に知らせる」。 Stateの更新そのものは親が担当する、という分担がとても大事です。

UserListは「表示+削除イベントの通知」に集中させる

次に、一覧表示側のコンポーネントを見てみます。

import UserItem from "./UserItem";

function UserList({ users, onDeleteUser }) {
  if (users.length === 0) {
    return <p>まだユーザーは登録されていません。</p>;
  }

  return (
    <div>
      <h3>登録済みユーザー一覧</h3>
      <ul style={{ paddingLeft: "16px" }}>
        {users.map((user) => (
          <UserItem
            key={user.id}
            user={user}
            onDelete={() => onDeleteUser(user.id)}
          />
        ))}
      </ul>
    </div>
  );
}

export default UserList;
JSX

UserList が受け取るPropsは、

  • users:親が持っているユーザー一覧
  • onDeleteUser:親が持っている削除用の関数

です。

UserItem は、1ユーザー分の表示と削除ボタンを担当します。

function UserItem({ user, onDelete }) {
  return (
    <li style={{ marginBottom: "4px" }}>
      <span>
        {user.name}{user.email}
      </span>
      <button style={{ marginLeft: "8px" }} onClick={onDelete}>
        削除
      </button>
    </li>
  );
}

export default UserItem;
JSX

ここでも、

  • user:表示するためのデータ
  • onDelete:削除イベントを親に伝えるための関数

というPropsを受け取り、 自分ではStateを持たず、「表示+イベント通知」に集中しています。

State Liftingを「実際に起きていること」として理解する

もしStateを子に置いていたらどうなるかを考える

ここで、あえて「悪い例」を考えてみます。

もし、UserForm が自分の中に users を持っていたらどうなるでしょうか。

function BadUserForm() {
  const [users, setUsers] = useState([]); // ← フォームが一覧を持っている

  // 入力・バリデーション・登録処理…
}
JSX

この場合、

  • UserListusers を知らない
  • 親コンポーネントも users を知らない
  • アプリ全体から見て「ユーザー一覧」がどこにあるか分かりづらい

という状態になります。

そこで、

「ユーザー一覧はフォームだけでなく一覧表示にも関係する。 だから、両方のコンポーネントの共通の親にStateを置こう。」

という判断をして、

  • users を親コンポーネントに移動する
  • フォームは「新しいユーザーを親に渡すだけ」にする
  • 一覧は「親から渡されたusersを表示するだけ」にする

という形にするのが、State Lifting(ステートリフティング)です。

State Liftingの流れを言葉で整理する

簡易ユーザー登録アプリで起きているState Liftingを、流れで整理すると次のようになります。

  1. 最初は「フォームが自分の中にユーザー一覧を持っている」状態を想像する
  2. 一覧表示コンポーネントもユーザー一覧を必要としていることに気づく
  3. 「両方から見える場所」にユーザー一覧を移動しようと考える
  4. UserFormUserList の共通の親である UserPageusers を移動する
  5. UserForm は「新しいユーザーを親に渡すだけ」に責務を絞る
  6. UserList は「親から渡されたusersを表示するだけ」に責務を絞る

この「状態を上に持ち上げる」動きが、State Liftingです。

重要な感覚:

State Liftingは、「誰がその状態を必要としているか」を考えた結果として行うものです。 なんとなく上に置くのではなく、「共有したいから共通の親に置く」という理由があるのがポイントです。

共通の親を「状態のハブ」として意識する

共通の親は「みんなが見たい状態の持ち主」

UserFormUserList の両方が「ユーザー一覧」を必要としているとき、 その状態を置くべき場所は、共通の親です。

簡易ユーザー登録アプリでは、それが UserPage でした。

function UserPage() {
  const [users, setUsers] = useState([]);

  const handleAddUser = (user) => {
    setUsers((prevUsers) => [...prevUsers, user]);
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー登録ページ</h2>

      <UserForm onAddUser={handleAddUser} />

      <UserList users={users} onDeleteUser={handleDeleteUser} />
    </div>
  );
}
JSX

ここでの構造は、

  • UserPage: ユーザー一覧の「持ち主」 追加・削除のロジックを担当する
  • UserForm: 新しいユーザーを作り、onAddUser を通じて親に渡す
  • UserList: 親から渡された users を表示し、削除イベントを onDeleteUser で親に伝える

という形になっています。

重要な感覚:

「複数のコンポーネントが同じ状態を必要としているなら、その状態は共通の親に置く」 これが、State Liftingと共通の親の考え方の中心です。

DAY 38 中盤のまとめ ― 「親のState/子のProps/State Lifting/共通の親」をコードで腑に落とす

DAY 38中盤で定着させてほしいポイントは次の通りです。

  • 親コンポーネントは、アプリ全体で意味を持つ状態(ユーザー一覧など)を持つ「状態のハブ」として振る舞うこと。
  • 子コンポーネントは、親から渡されたProps(値+関数)を使って「表示」と「イベント通知」に集中すること。
  • State Liftingは、「複数のコンポーネントで共有したい状態を、より上の共通の親に移動させること」であり、誰がその状態を必要としているかを考えた結果として行うこと。
  • 共通の親は、「みんなが見たい状態の持ち主」として、フォームや一覧表示など複数のコンポーネントをつなぐ役割を持つこと。

この感覚が少しずつ身についてくると、 フォームだけでなく、タブ切り替え・モーダル・ダッシュボードなど、 より複雑なUIでも「Stateはどこに置くべきか」を落ち着いて判断できるようになっていきます。


DAY 38:Stateはどこに置くか 後半 ― 「自分で判断できる状態設計」の感覚を育てる

DAY 38の後半では、

  • 親のState
  • 子のProps
  • State Lifting
  • 共通の親

という4つのキーワードを、「自分で設計判断できるレベル」まで引き上げることを目標にします。 ここからは、少し抽象度を上げて、状態設計そのものの考え方を丁寧に言語化していきます。

親のStateを「アプリの真ん中に置く」という発想

どのStateが「アプリ全体の意味」を持つかを考える

まず意識してほしいのは、

「このStateは、どのコンポーネントにとって意味があるのか?」

という問いです。

例えば、簡易ユーザー登録アプリでは、

  • 入力中の nameemail
  • バリデーション用の nameErroremailError
  • 登録済みユーザー一覧 users

という3種類の状態がありました。

ここで、

  • nameemail は「フォームの中だけ」で意味を持つ
  • nameErroremailError も「フォームの中だけ」で意味を持つ
  • users は「フォーム」と「一覧表示」の両方で意味を持つ

という違いがあります。

この違いが、「Stateはどこに置くか」を決めるうえでの重要なヒントになります。

「フォームだけで完結するState」と「アプリ全体で意味を持つState」を分ける

簡易ユーザー登録アプリを例にすると、次のように整理できます。

  • フォームだけで完結するState
    • name
    • email
    • nameError
    • emailError
  • アプリ全体で意味を持つState
    • users

この整理から、

  • フォームだけで完結するStateは、UserForm が持てばよい
  • アプリ全体で意味を持つStateは、UserPage(親)が持つべき

という判断が自然に導かれます。

重要なポイント:

「どこで意味を持つ状態か」を考えることで、 親に置くべきStateと、子に閉じ込めてよいStateが見えてきます。

子のPropsを「親とのインターフェース」として意識する

Propsは「親と子の約束ごと」

子コンポーネントが受け取るPropsは、

「親と子の間の約束ごと」

だと考えると分かりやすいです。

例えば、UserFormUserPage の関係は次のような約束になっています。

  • 親(UserPage)の約束:
    • onAddUser という関数を渡すから、 新しいユーザーができたらそれを呼んでほしい」
  • 子(UserForm)の約束:
    • 「新しいユーザーを作ったら、必ず onAddUser(user) を呼ぶ」

コードにするとこうなります。

// 親
function UserPage() {
  const [users, setUsers] = useState([]);

  const handleAddUser = (user) => {
    setUsers((prevUsers) => [...prevUsers, user]);
  };

  return <UserForm onAddUser={handleAddUser} />;
}

// 子
function UserForm({ onAddUser }) {
  // 入力・バリデーション…

  const handleSubmit = (e) => {
    e.preventDefault();

    // バリデーションを通過したと仮定
    const newUser = { id: Date.now(), name, email };

    onAddUser(newUser); // ← 親との約束を果たす
  };

  // JSX…
}
JSX

ここでのPropsは、

  • 「親が子に渡すもの」
  • 「子が親にイベントを知らせるための窓口」

という役割を持っています。

重要なポイント:

Propsは単なる「値の受け渡し」ではなく、 親と子の間の「責務の分担」を表現するためのインターフェースです。

State Liftingを「設計の判断プロセス」として理解する

State Liftingは「必要な場所を見てから決める」

State Lifting(ステートリフティング)は、

「この状態は、どのコンポーネントから見えた方がよいか?」

を考えた結果として行うものです。

例えば、次のような状況を想像してみてください。

  • 最初は、UserFormusers を持っていた
  • 後から「一覧表示コンポーネントでも users を使いたい」となった

このとき、

  • UserForm の中にある users は、UserList から見えない
  • 親コンポーネントも users を知らない

という状態になり、アプリ全体の状態管理が難しくなります。

そこで、

  1. users はフォームだけでなく一覧表示にも関係する」と気づく
  2. 「両方から見える場所に users を移動しよう」と考える
  3. UserFormUserList の共通の親である UserPageusers を移動する

という判断をするのが、State Liftingです。

State Liftingの「前」と「後」を比べてみる

State Lifting前(悪い例)

function UserForm() {
  const [users, setUsers] = useState([]); // ← フォームが一覧を持っている

  // 入力・バリデーション・登録処理…
}

function UserList() {
  // usersがどこにあるか分からない…
}
JSX

この状態では、

  • UserListusers を知らない
  • 親コンポーネントも users を知らない
  • アプリ全体から見て「ユーザー一覧」がどこにあるか分かりづらい

という問題があります。

State Lifting後(良い例)

function UserPage() {
  const [users, setUsers] = useState([]);

  const handleAddUser = (user) => {
    setUsers((prevUsers) => [...prevUsers, user]);
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <UserForm onAddUser={handleAddUser} />
      <UserList users={users} onDeleteUser={handleDeleteUser} />
    </div>
  );
}
JSX

この状態では、

  • users は親が持つ
  • フォームは「新しいユーザーを親に渡すだけ」
  • 一覧は「親から渡されたusersを表示するだけ」

という構造になり、状態の流れがとても分かりやすくなります。

重要なポイント:

State Liftingは、「誰がその状態を必要としているか」を見て、 その状態を「より上の共通の親」に移動させる設計判断です。

共通の親を「状態のハブ」として設計する

共通の親は「みんなが見たい状態の持ち主」

共通の親とは、

「複数のコンポーネントが共有したい状態を持つべき場所」

です。

簡易ユーザー登録アプリでは、

  • UserFormUserList の両方が「ユーザー一覧」を必要としている
  • その共通の親が UserPage

でした。

この構造をもう一度整理すると、次のようになります。

function UserPage() {
  const [users, setUsers] = useState([]);

  const handleAddUser = (user) => {
    setUsers((prevUsers) => [...prevUsers, user]);
  };

  const handleDeleteUser = (id) => {
    setUsers((prevUsers) => prevUsers.filter((user) => user.id !== id));
  };

  return (
    <div>
      <h2>ユーザー登録ページ</h2>

      <UserForm onAddUser={handleAddUser} />

      <UserList users={users} onDeleteUser={handleDeleteUser} />
    </div>
  );
}
JSX
  • UserPage: ユーザー一覧の「持ち主」 追加・削除のロジックを担当する
  • UserForm: 新しいユーザーを作り、onAddUser を通じて親に渡す
  • UserList: 親から渡された users を表示し、削除イベントを onDeleteUser で親に伝える

重要な感覚:

共通の親は、「みんなが見たい状態のハブ」として設計する。 その親が、複数の子コンポーネントをつなぐ役割を持つのが理想的です。

DAY 38 後半版「状態設計チェックリスト」

最後に、DAY 38後半の内容を、 実際に設計するときに使える「状態設計チェックリスト」としてまとめます。

親のStateチェック

  • このStateは、複数のコンポーネントから意味を持ちますか?
  • フォームだけでなく一覧表示や別のUIでも使われますか?
  • もしそうなら、そのStateは親に置くべきではありませんか?

子のPropsチェック

  • 子コンポーネントは、親から渡されたPropsを「表示」と「イベント通知」に使っていますか?
  • 子が勝手にStateを増やして、親から見えない状態を抱え込んでいませんか?
  • 親と子の間で「この関数を呼べばこういうことが起きる」という約束が明確ですか?

State Liftingチェック

  • あるStateが「別のコンポーネントからも見えた方がよい」と感じたことはありませんか?
  • そのとき、そのStateを「より上の共通の親」に移動することを検討しましたか?
  • 移動した結果、状態の流れが分かりやすくなりましたか?

共通の親チェック

  • 複数のコンポーネントが同じ状態を必要としている場合、その状態は共通の親に置かれていますか?
  • 共通の親は、「状態のハブ」として振る舞っていますか?
  • 親が「持ち主」、子が「利用者」という関係が保たれていますか?

DAY 38 後半のまとめ ― 「Stateはどこに置くべきか」を自分で決められるようになる

DAY 38後半で身につけてほしいのは、次のような感覚です。

  • Stateは「どこで意味を持つか」を基準に、親に置くか子に置くかを判断すること。
  • 親のStateと子のPropsは、「状態の持ち主」と「表示・イベント通知の担当者」という役割分担で考えること。
  • State Liftingは、「共有したい状態を共通の親に移動させる」という設計判断であり、誰がその状態を必要としているかを見て決めること。
  • 共通の親は、「みんなが見たい状態のハブ」として、複数のコンポーネントをつなぐ役割を持つこと。

この「状態設計の目」が育ってくると、 Reactでフォームを作るときも、画面を構成するときも、 ただ動くコードではなく、長く付き合えるコードを書けるようになっていきます。

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