モダンJavaScriptの基礎から始める挫折しないためのReact入門 60日コース | Stateを使いこなす - DAY 28:Stateの更新

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 28:Stateの更新 前半 ― 「一部だけ変える」感覚を身につける
  2. map()とは ― 「配列を別の形に変換する」ための道具
    1. map()の基本イメージ
  3. 対象データ検索 ― 「どの要素を更新するか」を特定する
    1. IDで対象を見つける
    2. map()の中で対象を探す
  4. 一部更新 ― 「オブジェクトの一部だけを変える」ための考え方
    1. オブジェクトを丸ごと書き換えない
    2. Spread構文で「コピー+一部変更」する
  5. 完了状態の変更 ― 実際のコンポーネントで使ってみる
    1. ToDoリストを題材にする
  6. コードをステップバイステップで分解する
    1. 1. 追加処理(復習)
    2. 2. 完了状態の変更(今回の主役)
    3. 3. 表示側で完了状態を反映する
  7. 練習用テンプレート:DAY 28 前半版「完了状態変更付きToDoリスト」
  8. DAY 28 前半のまとめ
  9. DAY 28:Stateの更新 中盤 ― 「mapで探して、部分的に変える」を体に染み込ませる
  10. map()を「更新用の変換器」として意識する
    1. 単なる「表示用」ではなく「更新用」にも使う
  11. 対象データ検索 ― 「どれを変えるか」を明確にする
    1. IDで対象を特定するのが基本形
    2. 「対象だけ変えて、他はそのまま」がポイント
  12. 一部更新 ― Spread構文で「コピー+上書き」する
    1. 直接書き換えない理由をもう一度
    2. 正しい形:Spread構文で新しいオブジェクトを作る
    3. map+Spreadで「一部更新の標準形」ができる
  13. 完了状態の変更 ― UIとStateのつながりを意識する
    1. 「ボタン → State更新 → 表示変更」の一本線
  14. 練習用テンプレート:DAY 28 中盤版「完了状態変更付きToDoリスト」
  15. DAY 28 中盤のまとめ
  16. DAY 28:Stateの更新(後半)― 「mapで更新する」を自分の標準パターンにする
  17. map()を「State更新の型」として固定する
    1. 表示でも更新でも、mapの役割は同じです
  18. 対象データ検索を「IDで行う」ことを標準にする
    1. indexではなくIDを使う理由を、後半でしっかり固める
    2. IDを使った安全な対象特定
  19. 一部更新を「コピー+上書き」の形で統一する
    1. 直接書き換えはしない、をもう一度強く意識する
    2. 正しい形:map+Spreadで「新しいオブジェクト」を返す
  20. 完了状態の変更を「UIとStateの往復」として理解する
    1. ボタンは「State更新のトリガー」
  21. 練習用テンプレート:DAY 28 後半版「完了状態変更付きToDoリスト」
  22. DAY 28 後半のまとめ ― 「map+ID+Spread+完了変更」を自分の型にする

DAY 28:Stateの更新 前半 ― 「一部だけ変える」感覚を身につける

DAY 28では、Stateの中の 「一部だけを更新する」 ことにフォーカスして学習していきます。 これまで「追加」「削除」を学んできましたが、現実のアプリでは「状態の変更」が非常に多く登場します。

例えば、タスク管理アプリでの「完了状態の変更」や、買い物リストでの「購入済みフラグの切り替え」などです。 このような「一部更新」を正しく書けるようになると、ReactのState操作が一気に自由になります。

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

  • map()
  • 対象データ検索
  • 一部更新
  • 完了状態の変更

これらを、タスク管理(ToDoリスト)を題材にして、ステップバイステップでかみ砕いていきます。

map()とは ― 「配列を別の形に変換する」ための道具

map()の基本イメージ

まずは、JavaScriptの map() メソッドから整理します。 map() は、「配列の各要素を変換して、新しい配列を作る」 メソッドです。

シンプルな例を見てみます。

const numbers = [1, 2, 3];

const doubled = numbers.map((n) => n * 2);

console.log(doubled); // [2, 4, 6]
JavaScript

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

  • numbers の各要素 n に対して n * 2 を計算し
  • その結果を集めた新しい配列 doubled を作っている
  • 元の配列 numbers は変更されていない

という流れです。

重要ポイント:

  • map() は「元の配列を壊さず、新しい配列を作る」メソッドです。
  • ReactのState更新では、「元のStateを直接変更しない」ことが重要なので、map() は「一部更新」と相性が抜群です。

対象データ検索 ― 「どの要素を更新するか」を特定する

IDで対象を見つける

Stateの中の「一部だけを更新する」には、 まず 「どの要素を更新するか」 を特定する必要があります。

タスク管理アプリの1件分のデータを、次のようなオブジェクトで表現してみます。

const task = {
  id: 1,
  title: "Reactの勉強",
  done: false,
};
JavaScript

このとき、

  • id: そのタスクを一意に識別するための番号
  • title: タスクの内容
  • done: 完了状態(true / false

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

完了状態を変更したいときは、「この id のタスクの done を変えたい」という形で考えます。

map()の中で対象を探す

map() を使って「対象だけを更新する」には、 次のようなパターンで書きます。

const idToToggle = 1;

const newTasks = tasks.map((task) =>
  task.id === idToToggle
    ? { ...task, done: !task.done }
    : task
);
JavaScript

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

  • tasks の各要素 task に対して
    • task.id === idToToggle(対象のIDかどうか)をチェックし
    • 対象なら { ...task, done: !task.done } で「コピー+一部変更」した新しいオブジェクトを返し
    • 対象でなければ、そのまま task を返す
  • その結果として、「対象のタスクだけ done が反転した新しい配列」ができる

という流れです。

ここが「対象データ検索+一部更新」の基本形になります。

一部更新 ― 「オブジェクトの一部だけを変える」ための考え方

オブジェクトを丸ごと書き換えない

ReactのStateにオブジェクトを入れている場合、 一部だけを変えたいときに、次のように書きたくなることがあります。

// ❌ やってはいけない例
task.done = !task.done;
JavaScript

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

理由は、

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

です。

Spread構文で「コピー+一部変更」する

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

const updatedTask = {
  ...task,
  done: !task.done,
};
JavaScript

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

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

という流れです。

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

完了状態の変更 ― 実際のコンポーネントで使ってみる

ToDoリストを題材にする

ここからは、実際のReactコンポーネントで「完了状態の変更」を実装してみます。 題材はシンプルなToDoリストです。

import { useState } from "react";

function TodoList() {
  const [title, setTitle] = useState("");
  const [tasks, setTasks] = useState([]);
  const [nextId, setNextId] = useState(1);

  const handleAddTask = () => {
    const trimmed = title.trim();
    if (trimmed === "") return;

    const newTask = {
      id: nextId,
      title: trimmed,
      done: false,
    };

    setTasks((prevTasks) => [...prevTasks, newTask]);
    setNextId((prevId) => prevId + 1);
    setTitle("");
  };

  const handleToggleDone = (id) => {
    setTasks((prevTasks) =>
      prevTasks.map((task) =>
        task.id === id ? { ...task, done: !task.done } : task
      )
    );
  };

  return (
    <div>
      <h2>ToDoリスト(Stateの更新 前半)</h2>

      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="やることを入力してください"
      />
      <button onClick={handleAddTask}>追加</button>

      <ul style={{ marginTop: "8px" }}>
        {tasks.map((task) => (
          <li key={task.id}>
            <button onClick={() => handleToggleDone(task.id)}>
              {task.done ? "✅" : "⬜️"}
            </button>
            <span
              style={{
                marginLeft: "8px",
                textDecoration: task.done ? "line-through" : "none",
              }}
            >
              {task.title}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;
JSX

コードをステップバイステップで分解する

1. 追加処理(復習)

const handleAddTask = () => {
  const trimmed = title.trim();
  if (trimmed === "") return;

  const newTask = {
    id: nextId,
    title: trimmed,
    done: false,
  };

  setTasks((prevTasks) => [...prevTasks, newTask]);
  setNextId((prevId) => prevId + 1);
  setTitle("");
};
JSX

ここはDAY 26・27で学んだ「追加」のパターンです。

  • 入力値を整形・バリデーションする
  • オブジェクトとして1件分のデータを作る
  • Spread構文で新しい配列を作り、Stateに保存する

という流れになっています。

2. 完了状態の変更(今回の主役)

const handleToggleDone = (id) => {
  setTasks((prevTasks) =>
    prevTasks.map((task) =>
      task.id === id ? { ...task, done: !task.done } : task
    )
  );
};
JSX

ここがDAY 28前半の中心です。

  • prevTasks.map((task) => ...) → 「今のタスク一覧」を1件ずつ見て、新しい配列を作る
  • task.id === id → 「今回完了状態を変えたいタスクかどうか」を判定する
  • 対象なら { ...task, done: !task.done } → 「コピー+一部変更」で done を反転した新しいオブジェクトを返す
  • 対象でなければ task をそのまま返す → 他のタスクは変更しない

結果として、

「対象のタスクだけ done が反転した新しい配列」

ができ、その配列がStateとして保存されます。

3. 表示側で完了状態を反映する

<ul style={{ marginTop: "8px" }}>
  {tasks.map((task) => (
    <li key={task.id}>
      <button onClick={() => handleToggleDone(task.id)}>
        {task.done ? "✅" : "⬜️"}
      </button>
      <span
        style={{
          marginLeft: "8px",
          textDecoration: task.done ? "line-through" : "none",
        }}
      >
        {task.title}
      </span>
    </li>
  ))}
</ul>
JSX

ここでは、

  • task.done ? "✅" : "⬜️" → 完了状態に応じて表示アイコンを切り替えています。
  • textDecoration: task.done ? "line-through" : "none" → 完了したタスクには取り消し線を表示しています。

重要ポイント:

  • 「完了状態の変更」は、Stateの更新と表示の両方に関わる操作です。
  • State側では done を反転し、表示側では done に応じて見た目を変えています。
  • どちらも「Stateの現在値」を元にしているため、 Stateが正しく更新されていれば、表示も自然に追従します。

練習用テンプレート:DAY 28 前半版「完了状態変更付きToDoリスト」

最後に、DAY 28前半の学習項目をすべて含んだテンプレートをまとめておきます。

import { useState } from "react";

function TodoList() {
  const [title, setTitle] = useState("");
  const [tasks, setTasks] = useState([]);
  const [nextId, setNextId] = useState(1);

  const handleAddTask = () => {
    const trimmed = title.trim();
    if (trimmed === "") return;

    const newTask = {
      id: nextId,
      title: trimmed,
      done: false,
    };

    setTasks((prevTasks) => [...prevTasks, newTask]);
    setNextId((prevId) => prevId + 1);
    setTitle("");
  };

  const handleToggleDone = (id) => {
    setTasks((prevTasks) =>
      prevTasks.map((task) =>
        task.id === id ? { ...task, done: !task.done } : task
      )
    );
  };

  return (
    <div>
      <h2>ToDoリスト(Stateの更新 前半)</h2>

      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="やることを入力してください"
      />
      <button onClick={handleAddTask}>追加</button>

      <ul style={{ marginTop: "8px" }}>
        {tasks.map((task) => (
          <li key={task.id}>
            <button onClick={() => handleToggleDone(task.id)}>
              {task.done ? "✅" : "⬜️"}
            </button>
            <span
              style={{
                marginLeft: "8px",
                textDecoration: task.done ? "line-through" : "none",
              }}
            >
              {task.title}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;
JSX

DAY 28 前半のまとめ

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

  • map() は「配列の各要素を変換して、新しい配列を作る」ためのメソッドであり、「一部更新」と相性が良いこと
  • 対象データの検索は、task.id === id のようにIDで行うのが基本であること
  • 一部更新は、Spread構文({ ...task, done: !task.done })で「コピー+一部変更」した新しいオブジェクトを作る形で行うこと
  • 完了状態の変更は、「State側で done を反転し、表示側で done に応じて見た目を変える」という二段構えで考えること

この「一部更新」のパターンが身につくと、

  • タスク管理
  • 購入済みフラグ
  • いいねボタン
  • フォロー状態の切り替え

など、あらゆる「状態の変更」を、落ち着いて・安全に・自信を持って書けるようになります。


DAY 28:Stateの更新 中盤 ― 「mapで探して、部分的に変える」を体に染み込ませる

DAY 28の中盤では、前半で触れた 「一部だけを更新する」State操作 を、 より実務に近い形で深く理解していきます。

テーマは引き続き 完了状態の変更(ToDoリスト) ですが、 ここでは次の4つを、もう一段階踏み込んで整理します。

  • map()
  • 対象データ検索
  • 一部更新
  • 完了状態の変更

「なんとなく分かる」から「自分で設計できる」まで持っていくイメージで読んでください。

map()を「更新用の変換器」として意識する

単なる「表示用」ではなく「更新用」にも使う

Reactを学び始めると、map() はまず「一覧表示」でよく登場します。

<ul>
  {tasks.map((task) => (
    <li key={task.id}>{task.title}</li>
  ))}
</ul>
JSX

ここでは、

  • 配列 tasks
  • JSX(<li>...</li>)に変換して
  • 画面に表示する

という用途で使っています。

中盤では、map()「State更新のための変換器」 としても意識してみます。

setTasks((prevTasks) =>
  prevTasks.map((task) =>
    task.id === id ? { ...task, done: !task.done } : task
  )
);
JSX

ここでは、

  • 配列 prevTasks
  • 「対象だけ一部変更された新しい配列」に変換して
  • Stateとして保存しています。

重要な気づき:

map() は「表示用」と「更新用」の両方で使える。 どちらも「配列を別の形に変換する」という同じ役割を持っている。

この感覚があると、map() が一気に頼れる相棒になります。

対象データ検索 ― 「どれを変えるか」を明確にする

IDで対象を特定するのが基本形

Stateの中の「一部だけを更新する」には、 まず 「どの要素を更新するか」 を特定する必要があります。

ToDoリストの1件分のデータは、次のようなオブジェクトだとします。

const task = {
  id: 3,
  title: "Reactの勉強",
  done: false,
};
JavaScript

完了状態を変えたいときは、

「idが3のタスクの done を反転したい」

という形で考えます。

この「対象の特定」を、map() の中で行います。

prevTasks.map((task) =>
  task.id === id ? { ...task, done: !task.done } : task
);
JSX

ここでの流れは、

  1. prevTasks の各 task を順番に見る
  2. task.id === id で「今回の対象かどうか」を判定する
  3. 対象なら「一部更新した新しいオブジェクト」を返す
  4. 対象でなければ、そのまま task を返す

というものです。

「対象だけ変えて、他はそのまま」がポイント

この書き方のポイントは、

  • 対象のタスクだけ { ...task, done: !task.done } で新しいオブジェクトにする
  • 他のタスクはそのまま task を返す

という「メリハリ」です。

ここが中盤で意識したいところです。

「全部を変える」のではなく、「対象だけ変えて、他はそのまま残す」。

この発想があると、複雑な更新も落ち着いて書けるようになります。

一部更新 ― Spread構文で「コピー+上書き」する

直接書き換えない理由をもう一度

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

// ❌ NG例
task.done = !task.done;
JSX

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

ReactのState更新では、これは避けるべきです。

理由は、

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

からです。

正しい形:Spread構文で新しいオブジェクトを作る

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

const updatedTask = {
  ...task,
  done: !task.done,
};
JSX

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

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

という流れです。

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

map+Spreadで「一部更新の標準形」ができる

これを map() と組み合わせると、 「一部更新の標準形」が完成します。

setTasks((prevTasks) =>
  prevTasks.map((task) =>
    task.id === id ? { ...task, done: !task.done } : task
  )
);
JSX

この1行の中に、

  • 対象データ検索(task.id === id
  • 一部更新({ ...task, done: !task.done }
  • 不変性の維持(新しい配列+新しいオブジェクト)

がすべて詰まっています。

完了状態の変更 ― UIとStateのつながりを意識する

「ボタン → State更新 → 表示変更」の一本線

完了状態の変更は、 UIとStateがきれいにつながる典型的な例です。

次のコードを見てください。

<ul style={{ marginTop: "8px" }}>
  {tasks.map((task) => (
    <li key={task.id}>
      <button onClick={() => handleToggleDone(task.id)}>
        {task.done ? "✅" : "⬜️"}
      </button>
      <span
        style={{
          marginLeft: "8px",
          textDecoration: task.done ? "line-through" : "none",
        }}
      >
        {task.title}
      </span>
    </li>
  ))}
</ul>
JSX

ここで起きていることを、流れで追ってみます。

  1. ユーザーが「チェックボタン」をクリックする
  2. onClick={() => handleToggleDone(task.id)} が呼ばれ、 対象の task.idhandleToggleDone に渡される
  3. handleToggleDone の中で、setTasksmap+Spread構文により、 対象タスクの done が反転した新しい配列がStateとして保存される
  4. Reactが再レンダリングし、tasks.map(...) が再評価される
  5. task.done に応じて、
    • ボタンの表示(✅ / ⬜️)
    • タイトルのスタイル(取り消し線あり / なし) が切り替わる

ここでの本質:

UIは「Stateの現在値」を映しているだけ。 Stateを正しく更新すれば、UIは自然に追従する。

この感覚があると、「画面をどう変えるか」ではなく、 「Stateをどう変えるか」 に集中できるようになります。

練習用テンプレート:DAY 28 中盤版「完了状態変更付きToDoリスト」

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

import { useState } from "react";

function TodoList() {
  const [title, setTitle] = useState("");
  const [tasks, setTasks] = useState([]);
  const [nextId, setNextId] = useState(1);

  const handleAddTask = () => {
    const trimmed = title.trim();
    if (trimmed === "") return;

    const newTask = {
      id: nextId,
      title: trimmed,
      done: false,
    };

    setTasks((prevTasks) => [...prevTasks, newTask]);
    setNextId((prevId) => prevId + 1);
    setTitle("");
  };

  const handleToggleDone = (id) => {
    setTasks((prevTasks) =>
      prevTasks.map((task) =>
        task.id === id ? { ...task, done: !task.done } : task
      )
    );
  };

  return (
    <div>
      <h2>ToDoリスト(Stateの更新 中盤)</h2>

      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="やることを入力してください"
      />
      <button onClick={handleAddTask}>追加</button>

      <ul style={{ marginTop: "8px" }}>
        {tasks.map((task) => (
          <li key={task.id}>
            <button onClick={() => handleToggleDone(task.id)}>
              {task.done ? "✅" : "⬜️"}
            </button>
            <span
              style={{
                marginLeft: "8px",
                textDecoration: task.done ? "line-through" : "none",
              }}
            >
              {task.title}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;
JSX

DAY 28 中盤のまとめ

DAY 28中盤で定着させたいのは、次の4つです。

  • map() は「表示用」だけでなく「更新用」としても使える変換器であり、配列を「一部だけ変えた新しい配列」にするのに最適であること
  • 対象データ検索 は、task.id === id のようにIDで行い、「対象だけ変えて、他はそのまま」というメリハリをつけること
  • 一部更新 は、Spread構文({ ...task, done: !task.done })で「コピー+一部上書き」した新しいオブジェクトを作る形で行い、元のオブジェクトを直接書き換えないこと
  • 完了状態の変更 は、「ボタン → State更新 → 表示変更」という一本の流れで考え、UIはStateの現在値を映しているだけだと理解すること

この「map+ID+Spread+完了状態変更」のパターンが体に染み込むと、

  • いいねボタン
  • フォロー状態の切り替え
  • フラグのON/OFF
  • 設定項目の有効/無効

など、あらゆる「一部だけ変える」操作を、 同じ型で落ち着いて書けるようになります。


DAY 28:Stateの更新(後半)― 「mapで更新する」を自分の標準パターンにする

DAY 28の後半では、 これまで学んできた map()+対象データ検索+一部更新+完了状態の変更 を、 「どんなアプリでも使える自分の標準パターン」に仕上げていきます。

題材は引き続き ToDoリスト ですが、 ここでは「完了状態の変更」を軸にしながら、 設計目線・安全性・拡張性 を意識して深掘りしていきます。

map()を「State更新の型」として固定する

表示でも更新でも、mapの役割は同じです

まず、表示側の map() をもう一度見てみます。

<ul>
  {tasks.map((task) => (
    <li key={task.id}>{task.title}</li>
  ))}
</ul>
JSX

ここでは、

  • tasks(配列)を
  • <li>...</li>(JSX)に変換して
  • 新しい配列(JSXの配列)を作っています。

一方、更新側の map() はこうでした。

setTasks((prevTasks) =>
  prevTasks.map((task) =>
    task.id === id ? { ...task, done: !task.done } : task
  )
);
JSX

ここでも、

  • prevTasks(配列)を
  • 「一部だけ更新された task オブジェクト」に変換して
  • 新しい配列(更新後のタスク一覧)を作っています。

共通している本質:

map() は「配列の各要素を変換して、新しい配列を作る」ための道具。 表示でも更新でも、やっていることは同じ。

この感覚があると、 「表示用のmap」と「更新用のmap」が頭の中で一本につながります。

対象データ検索を「IDで行う」ことを標準にする

indexではなくIDを使う理由を、後半でしっかり固める

完了状態を変更するとき、 対象のタスクを特定するために id を使っていました。

task.id === id
JSX

ここで、index を使う書き方と比較してみます。

// ❌ indexを使った危険な例
setTasks((prevTasks) =>
  prevTasks.map((task, index) =>
    index === indexToToggle ? { ...task, done: !task.done } : task
  )
);
JSX

一見動きそうですが、後半の段階ではこれは避けたい書き方です。

理由は、

  • 並び替えや途中削除が入ると、「index」と「本来変えたいタスク」がずれてしまう可能性がある
  • Reactの再レンダリング時に、key がindexだと要素の再利用が誤って行われることがある

など、後からバグの温床になりやすいからです。

IDを使った安全な対象特定

IDを使うと、対象を安定して特定できます。

setTasks((prevTasks) =>
  prevTasks.map((task) =>
    task.id === id ? { ...task, done: !task.done } : task
  )
);
JSX

ここを自分の標準にしてほしいです。

  • 「一覧の1件には必ずIDを持たせる」
  • 「対象の特定は id で行う」

この2つが習慣になると、 削除・更新・詳細表示など、どんな機能を追加してもブレなくなります。

一部更新を「コピー+上書き」の形で統一する

直接書き換えはしない、をもう一度強く意識する

完了状態の変更は、 オブジェクトの一部だけを変える操作です。

NGな書き方はこうでした。

// ❌ NG例:直接書き換え
const handleToggleDone = (id) => {
  setTasks((prevTasks) => {
    const found = prevTasks.find((task) => task.id === id);
    if (found) {
      found.done = !found.done; // 直接書き換え
    }
    return prevTasks;
  });
};
JSX

このコードは、

  • found が参照しているオブジェクトを直接書き換えている
  • prevTasks 自体は「同じ配列のまま」なので、 Reactが変化を検知しにくい

という問題があります。

正しい形:map+Spreadで「新しいオブジェクト」を返す

正しい形は、 map+Spread構文で「コピー+一部上書き」した新しいオブジェクトを返すことです。

const handleToggleDone = (id) => {
  setTasks((prevTasks) =>
    prevTasks.map((task) =>
      task.id === id ? { ...task, done: !task.done } : task
    )
  );
};
JSX

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

  • 対象のタスクだけ { ...task, done: !task.done } で新しいオブジェクトにする
  • 他のタスクはそのまま task を返す
  • 結果として、「一部だけ更新された新しい配列」ができる

という流れです。

後半で固めたいポイント:

「一部更新」は、 直接書き換えではなく「コピー+上書き」で行う。 そのための道具が Spread構文。

完了状態の変更を「UIとStateの往復」として理解する

ボタンは「State更新のトリガー」

完了状態の変更は、 UIとStateがきれいにつながる典型的な例です。

<ul style={{ marginTop: "8px" }}>
  {tasks.map((task) => (
    <li key={task.id}>
      <button onClick={() => handleToggleDone(task.id)}>
        {task.done ? "✅" : "⬜️"}
      </button>
      <span
        style={{
          marginLeft: "8px",
          textDecoration: task.done ? "line-through" : "none",
        }}
      >
        {task.title}
      </span>
    </li>
  ))}
</ul>
JSX

ここでの流れをもう一度整理します。

  1. ユーザーがボタンをクリックする
  2. onClick={() => handleToggleDone(task.id)} が呼ばれ、対象の task.id が渡される
  3. handleToggleDone の中で、setTasksmap+Spread構文により、対象タスクの done が反転した新しい配列がStateとして保存される
  4. Reactが再レンダリングし、tasks.map(...) が再評価される
  5. task.done に応じて、
    • ボタンの表示(✅ / ⬜️)
    • タイトルのスタイル(取り消し線あり / なし) が切り替わる

ここで定着させたい本質:

UIは「Stateの現在値」を映しているだけ。 Stateを正しく更新すれば、UIは自然に変わる。

この一本線が見えていると、 「画面をどういじるか」ではなく、 「Stateをどう設計して、どう更新するか」 に集中できるようになります。

練習用テンプレート:DAY 28 後半版「完了状態変更付きToDoリスト」

DAY 28後半の内容をすべて含んだテンプレートを示します。

import { useState } from "react";

function TodoList() {
  const [title, setTitle] = useState("");
  const [tasks, setTasks] = useState([]);
  const [nextId, setNextId] = useState(1);

  const handleAddTask = () => {
    const trimmed = title.trim();
    if (trimmed === "") return;

    const newTask = {
      id: nextId,
      title: trimmed,
      done: false,
    };

    setTasks((prevTasks) => [...prevTasks, newTask]);
    setNextId((prevId) => prevId + 1);
    setTitle("");
  };

  const handleToggleDone = (id) => {
    setTasks((prevTasks) =>
      prevTasks.map((task) =>
        task.id === id ? { ...task, done: !task.done } : task
      )
    );
  };

  return (
    <div>
      <h2>ToDoリスト(Stateの更新 後半)</h2>

      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="やることを入力してください"
      />
      <button onClick={handleAddTask}>追加</button>

      <ul style={{ marginTop: "8px" }}>
        {tasks.map((task) => (
          <li key={task.id}>
            <button onClick={() => handleToggleDone(task.id)}>
              {task.done ? "✅" : "⬜️"}
            </button>
            <span
              style={{
                marginLeft: "8px",
                textDecoration: task.done ? "line-through" : "none",
              }}
            >
              {task.title}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoList;
JSX

DAY 28 後半のまとめ ― 「map+ID+Spread+完了変更」を自分の型にする

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

  • map() は「配列を別の形に変換する」ための道具であり、表示でも更新でも同じ役割を持つこと
  • 対象データ検索 は、task.id === id のようにIDで行い、「対象だけ変えて、他はそのまま」というメリハリをつけること
  • 一部更新 は、Spread構文({ ...task, done: !task.done })で「コピー+一部上書き」した新しいオブジェクトを作る形で行い、直接書き換えないこと
  • 完了状態の変更 は、「ボタン → State更新 → 再レンダリング → 表示変更」という一本の流れで理解し、UIはStateの結果でしかないと捉えること

この「map+ID+Spread+完了状態変更」の型が自分の中で固まると、

  • いいね/いいね解除
  • フォロー/フォロー解除
  • ON/OFFスイッチ
  • 設定の有効/無効

など、あらゆる「一部だけ変える」操作を、 同じパターンで、落ち着いて、安全に書けるようになります。

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