モダンJavaScriptの基礎から始める挫折しないためのReact入門 60日コース | Stateを使いこなす - DAY 29:オブジェクトState

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 29:オブジェクトState 前半 ― 「1つのまとまりとして状態を持つ」感覚を身につける
  2. オブジェクトをStateで管理する ― 「1人のユーザー」を丸ごと持つ
    1. まずはシンプルなオブジェクトStateから
  3. Spread構文 ― 「コピー+一部上書き」のための基本ツール
    1. なぜオブジェクトStateにSpread構文が必要なのか
    2. Spread構文で「新しいオブジェクト」を作る
  4. 一部更新 ― 入力フォームからオブジェクトStateを更新する
    1. 名前入力欄を作る
  5. ネストしたデータ ― 「オブジェクトの中にオブジェクト」がある場合
    1. ネスト構造の例を考える
    2. ネストしたデータの一部更新
  6. 練習用テンプレート:DAY 29 前半版「オブジェクトStateプロフィールフォーム」
  7. DAY 29 前半のまとめ
  8. DAY 29:オブジェクトState 中盤 ― 「現実的なデータ構造」をReactで扱う感覚を育てる
  9. オブジェクトをStateで管理する ― 「1つの画面の状態」をまとめて持つ
    1. プロフィール+設定を1つのStateにまとめる
  10. Spread構文 ― 「壊さずに変える」ための必須テクニック
    1. 外側と内側、どちらも壊さない
  11. 一部更新 ― プロフィールと設定をそれぞれ安全に更新する
    1. プロフィールの一部更新(名前・メール)
    2. 設定の一部更新(テーマ・通知)
  12. ネストしたデータ ― 「画面全体の状態」を安全に扱う
    1. ネストが深くなっても考え方は同じ
  13. 練習用テンプレート:DAY 29 中盤版「プロフィール+設定画面」
  14. DAY 29 中盤のまとめ
  15. DAY 29:オブジェクトState(後半)― 「壊さずに育てる状態管理」を自分の標準にする
  16. オブジェクトStateを「画面の状態そのもの」として設計する
    1. 1つの画面に必要な情報を、1つのオブジェクトにまとめる
  17. Spread構文を「壊さずに育てるためのルール」として固定する
    1. 直接書き換えは絶対にしない、を後半で強く固める
  18. 一部更新を「型」として覚える
    1. タスクのタイトル・説明・期限を更新する
    2. UI状態(編集モード・保存中)を更新する
  19. ネストしたデータを「怖がらずに扱う」ための視点
    1. ネストは「階層構造」として落ち着いて読む
  20. 練習用テンプレート:DAY 29 後半版「タスク詳細+UI状態」
  21. DAY 29 後半のまとめ ― 「オブジェクトState+Spread+一部更新+ネスト」を自分の型にする

DAY 29:オブジェクトState 前半 ― 「1つのまとまりとして状態を持つ」感覚を身につける

DAY 29では、これまで配列Stateを中心に扱ってきた流れから一歩進んで、 「オブジェクトをStateで管理する」 ことにフォーカスして学習していきます。

現実のアプリでは、「1つのもの」に対して複数の情報が紐づくことがほとんどです。

  • ユーザー情報:名前・メールアドレス・年齢
  • プロフィール:自己紹介・アイコンURL・SNSリンク
  • 設定:テーマカラー・通知ON/OFF・言語

こうした「複数のプロパティを持つ1つのまとまり」を扱うときに、 オブジェクトStateが登場します。

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

  • オブジェクトをStateで管理
  • Spread構文
  • 一部更新
  • ネストしたデータ

これらを、ユーザープロフィールフォームを題材にして、 ステップバイステップでかみ砕いていきます。

オブジェクトをStateで管理する ― 「1人のユーザー」を丸ごと持つ

まずはシンプルなオブジェクトStateから

ユーザーのプロフィールを、次のようなオブジェクトで表現してみます。

const profile = {
  name: "山田太郎",
  email: "taro@example.com",
  age: 25,
};
JavaScript

これをReactのStateとして管理するときは、useState にそのまま渡します。

import { useState } from "react";

function ProfileForm() {
  const [profile, setProfile] = useState({
    name: "",
    email: "",
    age: "",
  });

  return (
    <div>
      <h2>プロフィール編集(オブジェクトState 前半)</h2>
      {/* ここにフォームを作っていきます */}
    </div>
  );
}

export default ProfileForm;
JSX

ここでのポイントは、

  • profile という1つのStateの中に、 nameemailage という複数のプロパティが入っていること
  • 「1人のユーザー」を1つのオブジェクトとして扱っていること

です。

重要な感覚:

「1つのもの」に複数の情報が紐づくときは、 それを1つのオブジェクトとしてStateに持つ。

この感覚があると、State設計がぐっと自然になります。

Spread構文 ― 「コピー+一部上書き」のための基本ツール

なぜオブジェクトStateにSpread構文が必要なのか

オブジェクトStateの中の一部だけを更新したいとき、 次のように書きたくなることがあります。

// ❌ NG例:直接書き換え
profile.name = "新しい名前";
setProfile(profile);
JSX

これは、元の profile オブジェクトを直接書き換えています。

ReactのState更新では、 元のオブジェクトを直接書き換えるのは避けるべきです。

理由は、

  • Reactは「前回のState」と「今回のState」の違いを、 オブジェクトの参照(別物かどうか)で判断することが多い
  • 中身だけ書き換えると、「同じオブジェクトのまま中身だけ変わった」状態になり、 差分検知が難しくなることがある

からです。

Spread構文で「新しいオブジェクト」を作る

正しい形は、 Spread構文(...)を使って「コピー+一部上書き」した新しいオブジェクトを作ることです。

const updatedProfile = {
  ...profile,
  name: "新しい名前",
};
setProfile(updatedProfile);
JSX

ここで起きていることは、

  • { ...profile } で、profile の全プロパティをコピーした新しいオブジェクトを作る
  • name: "新しい名前" で、その新しいオブジェクトの name だけを上書きする

という流れです。

この updatedProfile は、元の profile とは別のオブジェクトです。 そのため、Reactは「Stateが変わった」と認識しやすくなります。

ここが、オブジェクトStateにおけるSpread構文の本質です。

一部更新 ― 入力フォームからオブジェクトStateを更新する

名前入力欄を作る

実際にフォームを作りながら、一部更新を体に馴染ませていきます。

function ProfileForm() {
  const [profile, setProfile] = useState({
    name: "",
    email: "",
    age: "",
  });

  const handleChangeName = (e) => {
    const newName = e.target.value;
    setProfile((prev) => ({
      ...prev,
      name: newName,
    }));
  };

  return (
    <div>
      <h2>プロフィール編集(オブジェクトState 前半)</h2>

      <div>
        <label>
          名前:
          <input
            type="text"
            value={profile.name}
            onChange={handleChangeName}
          />
        </label>
      </div>
    </div>
  );
}
JSX

ここで起きていることを分解してみます。

  • value={profile.name} → Stateの profile.name を入力欄に表示しています。 入力欄はReactに「支配」されている状態です。
  • onChange={handleChangeName} → ユーザーが入力するたびにイベントが発生し、 handleChangeName が呼ばれます。
  • setProfile((prev) => ({ ...prev, name: newName })) → 以前の profileprev として受け取り、 Spread構文でコピーしつつ name だけを新しい値に上書きした 新しいオブジェクトをStateとして保存しています。

重要ポイント:

オブジェクトStateの一部更新は、 setProfile((prev) => ({ ...prev, 変更したいプロパティ: 新しい値 })) という形が基本パターンになる。

この形を、名前・メール・年齢など、 複数の入力欄で繰り返し使っていきます。

ネストしたデータ ― 「オブジェクトの中にオブジェクト」がある場合

ネスト構造の例を考える

現実のアプリでは、 オブジェクトの中にさらにオブジェクトが入る「ネストしたデータ」がよく登場します。

例えば、次のようなプロフィールデータです。

const profile = {
  name: "山田太郎",
  email: "taro@example.com",
  age: 25,
  address: {
    city: "東京",
    zip: "123-4567",
  },
};
JavaScript

ここでは、

  • profile が「ユーザー全体」
  • address が「住所情報」
  • address.cityaddress.zip が具体的な項目

という構造になっています。

ネストしたデータの一部更新

address.city だけを更新したいとき、 次のように書きたくなるかもしれません。

// ❌ NG例:直接書き換え
profile.address.city = "大阪";
setProfile(profile);
JSX

これは、profile.address の中身を直接書き換えています。 やはり、ReactのState更新としては避けるべきです。

正しい形は、 外側と内側の両方でSpread構文を使うことです。

setProfile((prev) => ({
  ...prev,
  address: {
    ...prev.address,
    city: "大阪",
  },
}));
JSX

ここで起きていることは、

  • ...prev で、元の profile をコピーした新しいオブジェクトを作る
  • address: { ...prev.address, city: "大阪" } で、
    • 元の address をコピーした新しいオブジェクトを作り
    • その中の city だけを上書きする

という二段構えです。

重要ポイント:

ネストしたデータの一部更新は、 「外側のオブジェクト」と「内側のオブジェクト」の両方を Spread構文でコピー+上書きする。

この形は少し長くなりますが、 「元のオブジェクトを壊さず、新しいオブジェクトを作る」という Reactの不変性のルールを守るために重要です。

練習用テンプレート:DAY 29 前半版「オブジェクトStateプロフィールフォーム」

最後に、DAY 29前半の学習項目をすべて含んだ シンプルなプロフィールフォームのテンプレートを示します。

import { useState } from "react";

function ProfileForm() {
  const [profile, setProfile] = useState({
    name: "",
    email: "",
    age: "",
    address: {
      city: "",
      zip: "",
    },
  });

  const handleChangeName = (e) => {
    const newName = e.target.value;
    setProfile((prev) => ({
      ...prev,
      name: newName,
    }));
  };

  const handleChangeEmail = (e) => {
    const newEmail = e.target.value;
    setProfile((prev) => ({
      ...prev,
      email: newEmail,
    }));
  };

  const handleChangeAge = (e) => {
    const newAge = e.target.value;
    setProfile((prev) => ({
      ...prev,
      age: newAge,
    }));
  };

  const handleChangeCity = (e) => {
    const newCity = e.target.value;
    setProfile((prev) => ({
      ...prev,
      address: {
        ...prev.address,
        city: newCity,
      },
    }));
  };

  const handleChangeZip = (e) => {
    const newZip = e.target.value;
    setProfile((prev) => ({
      ...prev,
      address: {
        ...prev.address,
        zip: newZip,
      },
    }));
  };

  return (
    <div>
      <h2>プロフィール編集(オブジェクトState 前半)</h2>

      <div>
        <label>
          名前:
          <input
            type="text"
            value={profile.name}
            onChange={handleChangeName}
          />
        </label>
      </div>

      <div>
        <label>
          メール:
          <input
            type="email"
            value={profile.email}
            onChange={handleChangeEmail}
          />
        </label>
      </div>

      <div>
        <label>
          年齢:
          <input
            type="number"
            value={profile.age}
            onChange={handleChangeAge}
          />
        </label>
      </div>

      <div>
        <label>
          住所(市区町村):
          <input
            type="text"
            value={profile.address.city}
            onChange={handleChangeCity}
          />
        </label>
      </div>

      <div>
        <label>
          郵便番号:
          <input
            type="text"
            value={profile.address.zip}
            onChange={handleChangeZip}
          />
        </label>
      </div>

      <hr />

      <div>
        <h3>現在のプロフィール</h3>
        <p>名前:{profile.name}</p>
        <p>メール:{profile.email}</p>
        <p>年齢:{profile.age}</p>
        <p>住所:{profile.address.city}</p>
        <p>郵便番号:{profile.address.zip}</p>
      </div>
    </div>
  );
}

export default ProfileForm;
JSX

DAY 29 前半のまとめ

DAY 29前半で身につけてほしいのは、次の感覚です。

  • オブジェクトをStateで管理することで、「1つのもの」に複数の情報が紐づく現実的なデータを自然に扱えること
  • Spread構文は、「元のオブジェクトを壊さず、新しいオブジェクトを作る」ための基本ツールであり、オブジェクトStateの一部更新に必須であること
  • 一部更新は、setProfile((prev) => ({ ...prev, プロパティ: 新しい値 })) という形が基本パターンになること
  • ネストしたデータの一部更新は、「外側」と「内側」の両方でSpread構文を使ってコピー+上書きする二段構えになること

このオブジェクトStateの扱い方が身につくと、

  • ユーザー情報
  • 設定画面
  • プロフィール編集
  • 詳細画面の編集

など、現実的なUIを支える「まとまりのある状態」を、 落ち着いて・安全に・拡張しやすい形で扱えるようになります。


DAY 29:オブジェクトState 中盤 ― 「現実的なデータ構造」をReactで扱う感覚を育てる

DAY 29の中盤では、前半で触れた オブジェクトState を、 より「現実のアプリに近い形」で扱えるようになることを目指します。

学習項目は同じく、

  • オブジェクトをStateで管理
  • Spread構文
  • 一部更新
  • ネストしたデータ

ですが、ここでは「設計目線」と「安全性」を意識しながら、 プロフィール+設定のような、少し複雑なオブジェクトを題材にして深掘りしていきます。

オブジェクトをStateで管理する ― 「1つの画面の状態」をまとめて持つ

プロフィール+設定を1つのStateにまとめる

中盤では、単なるプロフィールだけでなく、 「プロフィール+設定」を1つのオブジェクトStateで扱ってみます。

例えば、次のような構造です。

const initialState = {
  profile: {
    name: "",
    email: "",
  },
  settings: {
    theme: "light",
    notifications: true,
  },
};
JavaScript

これをReactのStateとして持つと、こうなります。

import { useState } from "react";

function UserPage() {
  const [userState, setUserState] = useState({
    profile: {
      name: "",
      email: "",
    },
    settings: {
      theme: "light",
      notifications: true,
    },
  });

  return (
    <div>
      <h2>ユーザー画面(オブジェクトState 中盤)</h2>
      {/* ここにフォームや設定UIを作っていきます */}
    </div>
  );
}

export default UserPage;
JSX

ここでのポイントは、

  • userState という1つのStateの中に、 profilesettings という2つの「まとまり」が入っていること
  • 「この画面に必要な状態」を1つのオブジェクトとしてまとめていること

です。

大事な感覚:

「この画面で扱う状態」を、1つのオブジェクトにまとめて持つ。 その中に、さらに小さなまとまり(profile・settingsなど)を入れていく。

これが、現実的なUIのState設計の入り口になります。

Spread構文 ― 「壊さずに変える」ための必須テクニック

外側と内側、どちらも壊さない

userState の中の一部だけを更新したいとき、 例えば「名前だけ変えたい」とします。

NGな書き方はこうです。

// ❌ NG例:直接書き換え
userState.profile.name = "新しい名前";
setUserState(userState);
JSX

これは、

  • userState.profile の中身を直接書き換えている
  • userState 自体は「同じオブジェクトのまま」なので、 Reactが変化を検知しにくい

という問題があります。

正しい形は、 外側と内側の両方でSpread構文を使うことです。

setUserState((prev) => ({
  ...prev,
  profile: {
    ...prev.profile,
    name: "新しい名前",
  },
}));
JSX

ここで起きていることは、

  • ...prev で、元の userState をコピーした新しいオブジェクトを作る
  • profile: { ...prev.profile, name: "新しい名前" } で、
    • 元の profile をコピーした新しいオブジェクトを作り
    • その中の name だけを上書きする

という二段構えです。

重要ポイント:

ネストしたオブジェクトを更新するときは、 「外側のオブジェクト」と「内側のオブジェクト」の両方を Spread構文でコピー+上書きする。

これが「壊さずに変える」ための基本ルールになります。

一部更新 ― プロフィールと設定をそれぞれ安全に更新する

プロフィールの一部更新(名前・メール)

実際にフォームを作りながら、一部更新を体に馴染ませていきます。

function UserPage() {
  const [userState, setUserState] = useState({
    profile: {
      name: "",
      email: "",
    },
    settings: {
      theme: "light",
      notifications: true,
    },
  });

  const handleChangeName = (e) => {
    const newName = e.target.value;
    setUserState((prev) => ({
      ...prev,
      profile: {
        ...prev.profile,
        name: newName,
      },
    }));
  };

  const handleChangeEmail = (e) => {
    const newEmail = e.target.value;
    setUserState((prev) => ({
      ...prev,
      profile: {
        ...prev.profile,
        email: newEmail,
      },
    }));
  };

  return (
    <div>
      <h2>ユーザー画面(オブジェクトState 中盤)</h2>

      <div>
        <label>
          名前:
          <input
            type="text"
            value={userState.profile.name}
            onChange={handleChangeName}
          />
        </label>
      </div>

      <div>
        <label>
          メール:
          <input
            type="email"
            value={userState.profile.email}
            onChange={handleChangeEmail}
          />
        </label>
      </div>
    </div>
  );
}
JSX

ここでの流れは、

  • 入力欄の valueuserState.profile.name / email を渡す
  • onChange で新しい値を受け取り、 setUserState+Spread構文で「profileだけ一部更新した新しいオブジェクト」を作る
  • その新しいオブジェクトをStateとして保存し、Reactが再レンダリングする

というものです。

設定の一部更新(テーマ・通知)

設定側も同じパターンで更新できます。

const handleToggleNotifications = () => {
  setUserState((prev) => ({
    ...prev,
    settings: {
      ...prev.settings,
      notifications: !prev.settings.notifications,
    },
  }));
};

const handleChangeTheme = (e) => {
  const newTheme = e.target.value; // "light" or "dark"
  setUserState((prev) => ({
    ...prev,
    settings: {
      ...prev.settings,
      theme: newTheme,
    },
  }));
};
JSX

表示側はこうなります。

<div>
  <label>
    テーマ:
    <select
      value={userState.settings.theme}
      onChange={handleChangeTheme}
    >
      <option value="light">ライト</option>
      <option value="dark">ダーク</option>
    </select>
  </label>
</div>

<div>
  <label>
    通知:
    <input
      type="checkbox"
      checked={userState.settings.notifications}
      onChange={handleToggleNotifications}
    />
  </label>
</div>
JSX

ここで意識したいこと:

  • プロフィールも設定も、 「外側のState(userState)」の中の一部として扱っていること
  • どちらも、 setUserState((prev) => ({ ...prev, 内側: { ...prev.内側, プロパティ: 新しい値 } })) という同じ型で更新していること

この「同じ型で書ける」感覚が、中盤で育てたいところです。

ネストしたデータ ― 「画面全体の状態」を安全に扱う

ネストが深くなっても考え方は同じ

ネストしたデータは、 深くなればなるほどSpread構文の入れ子が増えます。

例えば、さらに「通知の詳細設定」が入った場合を考えてみます。

const userState = {
  profile: { name: "", email: "" },
  settings: {
    theme: "light",
    notifications: true,
    notificationDetail: {
      email: true,
      push: false,
    },
  },
};
JavaScript

notificationDetail.email だけを更新したいときは、こうなります。

setUserState((prev) => ({
  ...prev,
  settings: {
    ...prev.settings,
    notificationDetail: {
      ...prev.settings.notificationDetail,
      email: !prev.settings.notificationDetail.email,
    },
  },
}));
JSX

長くはなりますが、やっていることは前と同じです。

  • 外側(userState)をSpreadでコピー
  • その中の settings をSpreadでコピー
  • さらにその中の notificationDetail をSpreadでコピー
  • 最後に email だけを上書き

という流れです。

重要ポイント:

ネストが深くなっても、 「外側から順番にSpreadでコピー+上書きする」 というルールは変わらない。

このルールを守ることで、 「どこかで元のオブジェクトを壊してしまう」事故を防げます。

練習用テンプレート:DAY 29 中盤版「プロフィール+設定画面」

中盤の内容をすべて含んだテンプレートを示します。

import { useState } from "react";

function UserPage() {
  const [userState, setUserState] = useState({
    profile: {
      name: "",
      email: "",
    },
    settings: {
      theme: "light",
      notifications: true,
    },
  });

  const handleChangeName = (e) => {
    const newName = e.target.value;
    setUserState((prev) => ({
      ...prev,
      profile: {
        ...prev.profile,
        name: newName,
      },
    }));
  };

  const handleChangeEmail = (e) => {
    const newEmail = e.target.value;
    setUserState((prev) => ({
      ...prev,
      profile: {
        ...prev.profile,
        email: newEmail,
      },
    }));
  };

  const handleChangeTheme = (e) => {
    const newTheme = e.target.value;
    setUserState((prev) => ({
      ...prev,
      settings: {
        ...prev.settings,
        theme: newTheme,
      },
    }));
  };

  const handleToggleNotifications = () => {
    setUserState((prev) => ({
      ...prev,
      settings: {
        ...prev.settings,
        notifications: !prev.settings.notifications,
      },
    }));
  };

  return (
    <div>
      <h2>ユーザー画面(オブジェクトState 中盤)</h2>

      <div>
        <label>
          名前:
          <input
            type="text"
            value={userState.profile.name}
            onChange={handleChangeName}
          />
        </label>
      </div>

      <div>
        <label>
          メール:
          <input
            type="email"
            value={userState.profile.email}
            onChange={handleChangeEmail}
          />
        </label>
      </div>

      <div>
        <label>
          テーマ:
          <select
            value={userState.settings.theme}
            onChange={handleChangeTheme}
          >
            <option value="light">ライト</option>
            <option value="dark">ダーク</option>
          </select>
        </label>
      </div>

      <div>
        <label>
          通知:
          <input
            type="checkbox"
            checked={userState.settings.notifications}
            onChange={handleToggleNotifications}
          />
        </label>
      </div>

      <hr />

      <div>
        <h3>現在の状態</h3>
        <p>名前:{userState.profile.name}</p>
        <p>メール:{userState.profile.email}</p>
        <p>テーマ:{userState.settings.theme}</p>
        <p>通知:{userState.settings.notifications ? "ON" : "OFF"}</p>
      </div>
    </div>
  );
}

export default UserPage;
JSX

DAY 29 中盤のまとめ

DAY 29中盤で、自分の中に定着させてほしいのは次の4つです。

  • オブジェクトをStateで管理することで、「この画面に必要な状態」を1つのまとまりとして扱えること
  • Spread構文は、「外側」と「内側」の両方で使うことで、ネストしたオブジェクトを壊さずに更新できること
  • 一部更新は、setState((prev) => ({ ...prev, 内側: { ...prev.内側, プロパティ: 新しい値 } })) という型で統一できること
  • ネストしたデータは、深くなっても「外側から順番にコピー+上書きする」というルールを守れば、安全に扱えること

このオブジェクトStateの扱い方が身につくと、

  • プロフィール画面
  • 設定画面
  • ダッシュボード
  • 複数タブを持つ画面

など、「画面全体の状態」をReactでしっかり管理できるようになっていきます。


DAY 29:オブジェクトState(後半)― 「壊さずに育てる状態管理」を自分の標準にする

DAY 29の後半では、 これまで学んできた オブジェクトState+Spread構文+一部更新+ネストしたデータ を、 「どんな画面でも通用する自分の型」に仕上げていきます。

ここまでで、

  • オブジェクトをStateで持つ
  • Spread構文で一部更新する
  • ネストしたデータを外側・内側両方コピーして更新する

という基本はすでに触れています。

後半では、それを 「実際のアプリで使えるレベル」 にまで引き上げるイメージで読んでください。

オブジェクトStateを「画面の状態そのもの」として設計する

1つの画面に必要な情報を、1つのオブジェクトにまとめる

後半では、オブジェクトStateを単なる「便利な入れ物」ではなく、 「画面の状態そのもの」 として意識してみます。

例えば、次のような「タスク詳細画面」を考えます。

  • タスクのタイトル・説明・期限
  • タスクの完了状態
  • タスクに紐づく担当者情報
  • 画面上のUI状態(編集モードかどうか、保存中かどうか)

これを1つのStateにまとめると、こうなります。

const initialTaskState = {
  task: {
    id: 1,
    title: "",
    description: "",
    dueDate: "",
    done: false,
    assignee: {
      name: "",
      email: "",
    },
  },
  ui: {
    isEditing: false,
    isSaving: false,
  },
};
JavaScript

Reactコンポーネントではこう書きます。

import { useState } from "react";

function TaskDetailPage() {
  const [state, setState] = useState({
    task: {
      id: 1,
      title: "",
      description: "",
      dueDate: "",
      done: false,
      assignee: {
        name: "",
        email: "",
      },
    },
    ui: {
      isEditing: false,
      isSaving: false,
    },
  });

  return (
    <div>
      <h2>タスク詳細(オブジェクトState 後半)</h2>
      {/* ここにフォームやボタンを作っていきます */}
    </div>
  );
}

export default TaskDetailPage;
JSX

ここで意識してほしいのは、

  • state という1つのオブジェクトが、 「この画面に必要なすべての状態」を持っていること
  • taskui という2つの大きなまとまりがあり、 それぞれがさらにネストしたデータを持っていること

です。

重要な感覚:

オブジェクトStateは、「画面の状態」を丸ごと表現するための器。 その中に、意味のあるまとまりごとにネストしていく。

この設計目線があると、Stateが「散らばる」のではなく、 1つの軸で整理されていきます。

Spread構文を「壊さずに育てるためのルール」として固定する

直接書き換えは絶対にしない、を後半で強く固める

ネストしたオブジェクトを扱うとき、 ついこう書きたくなることがあります。

// ❌ NG例:直接書き換え
state.task.title = "新しいタイトル";
setState(state);
JSX

これは、

  • state.task の中身を直接書き換えている
  • state 自体は「同じオブジェクトのまま」なので、 Reactが変化を検知しにくい

という問題があります。

後半では、ここを 「絶対にやらない」 と自分の中で決めてしまうのがおすすめです。

代わりに、必ず Spread構文で「コピー+上書き」します。

setState((prev) => ({
  ...prev,
  task: {
    ...prev.task,
    title: "新しいタイトル",
  },
}));
JSX

さらに、担当者のメールアドレスだけ変えたい場合はこうです。

setState((prev) => ({
  ...prev,
  task: {
    ...prev.task,
    assignee: {
      ...prev.task.assignee,
      email: "new-email@example.com",
    },
  },
}));
JSX

ここで固めたいルール:

ネストしたオブジェクトを更新するときは、 外側から順番に Spread でコピー+上書きする。 直接書き換えはしない。

このルールを守ることで、

  • 「どこかで元のオブジェクトを壊してしまう」
  • 「画面が更新されない」

といった事故を避けられます。

一部更新を「型」として覚える

タスクのタイトル・説明・期限を更新する

実際にフォームを作りながら、 一部更新の「型」を体に染み込ませていきます。

function TaskDetailPage() {
  const [state, setState] = useState({
    task: {
      id: 1,
      title: "",
      description: "",
      dueDate: "",
      done: false,
      assignee: {
        name: "",
        email: "",
      },
    },
    ui: {
      isEditing: false,
      isSaving: false,
    },
  });

  const handleChangeTitle = (e) => {
    const newTitle = e.target.value;
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        title: newTitle,
      },
    }));
  };

  const handleChangeDescription = (e) => {
    const newDescription = e.target.value;
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        description: newDescription,
      },
    }));
  };

  const handleChangeDueDate = (e) => {
    const newDueDate = e.target.value;
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        dueDate: newDueDate,
      },
    }));
  };

  return (
    <div>
      <h2>タスク詳細(オブジェクトState 後半)</h2>

      <div>
        <label>
          タイトル:
          <input
            type="text"
            value={state.task.title}
            onChange={handleChangeTitle}
          />
        </label>
      </div>

      <div>
        <label>
          説明:
          <textarea
            value={state.task.description}
            onChange={handleChangeDescription}
          />
        </label>
      </div>

      <div>
        <label>
          期限:
          <input
            type="date"
            value={state.task.dueDate}
            onChange={handleChangeDueDate}
          />
        </label>
      </div>
    </div>
  );
}
JSX

ここでの共通パターンは、

setState((prev) => ({
  ...prev,
  task: {
    ...prev.task,
    プロパティ: 新しい値,
  },
}));
JSX

という形です。

UI状態(編集モード・保存中)を更新する

UI側の状態も同じ型で更新できます。

const toggleEditing = () => {
  setState((prev) => ({
    ...prev,
    ui: {
      ...prev.ui,
      isEditing: !prev.ui.isEditing,
    },
  }));
};

const setSaving = (isSaving) => {
  setState((prev) => ({
    ...prev,
    ui: {
      ...prev.ui,
      isSaving,
    },
  }));
};
JSX

表示側はこうなります。

<button onClick={toggleEditing}>
  {state.ui.isEditing ? "編集終了" : "編集する"}
</button>

{state.ui.isSaving && <p>保存中です...</p>}
JSX

ここで定着させたいこと:

「どの部分を変えたいか」に応じて、 その部分までSpreadで降りていき、 最後にプロパティを上書きする。 それがオブジェクトStateの一部更新の型。

ネストしたデータを「怖がらずに扱う」ための視点

ネストは「階層構造」として落ち着いて読む

ネストしたデータは、 見た目のコードが長くなるので、最初は少し怖く感じるかもしれません。

例えば、担当者の名前だけ変えたい場合はこうです。

const handleChangeAssigneeName = (e) => {
  const newName = e.target.value;
  setState((prev) => ({
    ...prev,
    task: {
      ...prev.task,
      assignee: {
        ...prev.task.assignee,
        name: newName,
      },
    },
  }));
};
JSX

ここを「階層構造」として読むと、落ち着いて理解できます。

  • 最上位:state
  • その中の:task
  • さらにその中の:assignee
  • 最後に:name を上書き

という階段を降りているイメージです。

ポイント:

ネストしたデータは、「どの階層まで降りていくか」を意識して読む。 Spread構文は、その階層ごとに「壊さずコピーする」ための道具。

この視点があると、 ネストが深くなっても「ただ階段が増えただけ」と感じられるようになります。

練習用テンプレート:DAY 29 後半版「タスク詳細+UI状態」

DAY 29後半の内容をすべて含んだテンプレートをまとめます。

import { useState } from "react";

function TaskDetailPage() {
  const [state, setState] = useState({
    task: {
      id: 1,
      title: "",
      description: "",
      dueDate: "",
      done: false,
      assignee: {
        name: "",
        email: "",
      },
    },
    ui: {
      isEditing: false,
      isSaving: false,
    },
  });

  const handleChangeTitle = (e) => {
    const newTitle = e.target.value;
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        title: newTitle,
      },
    }));
  };

  const handleChangeDescription = (e) => {
    const newDescription = e.target.value;
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        description: newDescription,
      },
    }));
  };

  const handleChangeDueDate = (e) => {
    const newDueDate = e.target.value;
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        dueDate: newDueDate,
      },
    }));
  };

  const handleChangeAssigneeName = (e) => {
    const newName = e.target.value;
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        assignee: {
          ...prev.task.assignee,
          name: newName,
        },
      },
    }));
  };

  const handleChangeAssigneeEmail = (e) => {
    const newEmail = e.target.value;
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        assignee: {
          ...prev.task.assignee,
          email: newEmail,
        },
      },
    }));
  };

  const toggleEditing = () => {
    setState((prev) => ({
      ...prev,
      ui: {
        ...prev.ui,
        isEditing: !prev.ui.isEditing,
      },
    }));
  };

  const toggleDone = () => {
    setState((prev) => ({
      ...prev,
      task: {
        ...prev.task,
        done: !prev.task.done,
      },
    }));
  };

  return (
    <div>
      <h2>タスク詳細(オブジェクトState 後半)</h2>

      <button onClick={toggleEditing}>
        {state.ui.isEditing ? "編集終了" : "編集する"}
      </button>

      <div>
        <label>
          タイトル:
          <input
            type="text"
            value={state.task.title}
            onChange={handleChangeTitle}
            disabled={!state.ui.isEditing}
          />
        </label>
      </div>

      <div>
        <label>
          説明:
          <textarea
            value={state.task.description}
            onChange={handleChangeDescription}
            disabled={!state.ui.isEditing}
          />
        </label>
      </div>

      <div>
        <label>
          期限:
          <input
            type="date"
            value={state.task.dueDate}
            onChange={handleChangeDueDate}
            disabled={!state.ui.isEditing}
          />
        </label>
      </div>

      <div>
        <label>
          担当者名:
          <input
            type="text"
            value={state.task.assignee.name}
            onChange={handleChangeAssigneeName}
            disabled={!state.ui.isEditing}
          />
        </label>
      </div>

      <div>
        <label>
          担当者メール:
          <input
            type="email"
            value={state.task.assignee.email}
            onChange={handleChangeAssigneeEmail}
            disabled={!state.ui.isEditing}
          />
        </label>
      </div>

      <div>
        <label>
          完了:
          <input
            type="checkbox"
            checked={state.task.done}
            onChange={toggleDone}
          />
        </label>
      </div>

      <hr />

      <div>
        <h3>現在の状態</h3>
        <p>タイトル:{state.task.title}</p>
        <p>説明:{state.task.description}</p>
        <p>期限:{state.task.dueDate}</p>
        <p>担当者:{state.task.assignee.name}</p>
        <p>担当者メール:{state.task.assignee.email}</p>
        <p>完了:{state.task.done ? "はい" : "いいえ"}</p>
        <p>編集モード:{state.ui.isEditing ? "ON" : "OFF"}</p>
      </div>
    </div>
  );
}

export default TaskDetailPage;
JSX

DAY 29 後半のまとめ ― 「オブジェクトState+Spread+一部更新+ネスト」を自分の型にする

DAY 29後半で、自分の中に固定してほしいのは次の4つです。

  • オブジェクトをStateで管理することで、「画面の状態」を1つのオブジェクトとして表現できること
  • Spread構文は、「壊さずに育てる」ためのルールであり、外側から順番にコピー+上書きすることでネストしたデータも安全に更新できること
  • 一部更新は、setState((prev) => ({ ...prev, 内側: { ...prev.内側, プロパティ: 新しい値 } })) という型で統一できること
  • ネストしたデータは、「階層構造」として落ち着いて読み、どの階層まで降りていくかを意識しながらSpreadを重ねていけば怖くないこと

この型が自分の中で固まると、

  • タスク詳細画面
  • プロフィール+設定画面
  • 複雑なフォーム
  • ダッシュボードの状態管理

など、現実的なアプリの「状態の塊」を、 Reactで落ち着いて、安全に、拡張しやすく扱えるようになっていきます。

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