メインコンテンツまでスキップ

第39章:「元のデータを直接変えちゃダメ」っていう大事なルール

この章では、Reactの超・超・超大事な約束ごとをやります。 ここをちゃんと理解しておくと、

  • 画面が「え、なんで今変わった?」ってバグが減る ✨
  • 後の「配列のState」「オブジェクトのState」もスルスルわかる 💡
  • useReduceruseImmer を使うときも理解が深まる 🧠

という、かなりおいしい内容です。


1️⃣ まず結論:「Stateはコピーしてからさわる」ルール

Reactでは、

State(状態)は、元のデータはそのままにして、コピーを作ってから変更する

というルールがあります。

イメージとしては:

  • ❌ ノートに直接ぐちゃぐちゃ書き直す
  • ✅ コピーを取って、コピーのほうに書き直す

みたいな感じです 📒✏️

Reactでやってはいけないのは、たとえばこんな感じ👇

  • user.age++ みたいに Stateの中身を直接書き換える
  • todos.push(...)Stateの配列を直接いじる

こういうのは一見動きそうなんだけど、バグのタネになります…😇


2️⃣ Stateの流れをイメージしてみよう 🌊

まずは「ReactのStateってどう扱われてるの?」をざっくり図でイメージしてみましょう。

ポイントはここ👇

  • Reactは 「古いState」と「新しいState」が別物かどうか を見て、
  • 「おっ、変わった!」と思ったら画面を描き直しています 👀

なので、

  • 古いStateオブジェクトそのものを直接いじってしまうと
  • Reactから見ると「同じもの(同じ箱)」に見えてしまうことがあるんです 😵

3️⃣ これがNGコード:元のデータを直接いじっちゃう例 🚫

🔹 例1:オブジェクトのStateを直接いじるパターン

const [user, setUser] = useState({
name: "Yui",
age: 20,
});

const handleBirthday = () => {
// ❌ NG:元のオブジェクトを直接書き換えている
user.age = user.age + 1;
setUser(user);
};

一見「setUser(user) してるし良くない?」って思うかもですが、

  • userずっと同じオブジェクト なので、
  • Reactから見ると「前も今も同じもの」と判断されることがあります。

→ その結果、画面が更新されなかったり、挙動が不安定になったりします 🥲


🔹 例2:配列のStateを .push でいじるパターン

type Todo = {
id: number;
title: string;
};

const [todos, setTodos] = useState<Todo[]>([]);

const handleAddTodo = () => {
const newTodo: Todo = {
id: Date.now(),
title: "新しいTODO",
};

// ❌ NG:元の配列を直接いじっている
todos.push(newTodo);
setTodos(todos);
};

push元の配列を書き換えるメソッドです。

こういうコードも「前と同じ配列に見えるかも」問題を引き起こします ⚠️


4️⃣ 正しい書き方:新しいオブジェクト/配列を作ろう ✅

じゃあどう書けばいいの?というと、

「前のStateから、新しいStateを“作る”」

という書き方にします 🧪


🔹 オブジェクトStateの正しい更新例

const [user, setUser] = useState({
name: "Yui",
age: 20,
});

const handleBirthday = () => {
// ✅ OK:前のStateをもとに、新しいオブジェクトを作っている
setUser((prevUser) => ({
...prevUser, // いったん全部コピーして…
age: prevUser.age + 1, // 上書きしたいところだけ変える
}));
};

ここで出てきた ...prevUserスプレッド構文ってやつで、

  • prevUser のプロパティを全部コピーして
  • そのあとに age だけ上書きしている

という動きになっています 👌


🔹 配列Stateの正しい更新例(追加)

type Todo = {
id: number;
title: string;
};

const [todos, setTodos] = useState<Todo[]>([]);

const handleAddTodo = () => {
const newTodo: Todo = {
id: Date.now(),
title: "新しいTODO",
};

// ✅ OK:新しい配列を作っている
setTodos((prevTodos) => [...prevTodos, newTodo]);
};

[...prevTodos, newTodo] は、

  • prevTodos の中身を全部コピーして
  • 最後に newTodo をくっつけた「新しい配列」

というイメージです 📦📦📦➕📦


🔹 配列Stateの正しい更新例(削除)

const handleDeleteTodo = (id: number) => {
// ✅ OK:filter は新しい配列を返してくれる
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
};

filter も、元の配列をいじらず 新しく配列を作ってくれるメソッドです ✨


5️⃣ なんでここまで「直接さわるな」って言うの?🤔

理由はいくつかありますが、よく使うものを3つだけ 🎓

① Reactが変化に気づけなくなるから

Reactはざっくりいうと、

  • 「前のState」と「今のState」が同じ箱かどうか(参照)
  • を見て、「違う!」と思ったときに再描画

をしています。

直接書き換えると、

  • 「中身は変わってるけど、箱は同じ」なので
  • Reactが「ふーん、同じだね」とスルーしちゃうことがある 🫢

② 「過去のState」を安全に持っておきたいから

あとから出てくる

  • useReducer
  • useImmer
  • ログ・デバッグ・「戻る」機能 など

では、

それぞれのタイミングのStateを「履歴」として残しておきたい

ことがあります。

元のStateを直接いじってしまうと、

  • 「過去のはずのState」まで書き換わってしまって
  • 履歴がぐちゃぐちゃになります 💣

③ バグを見つけるのがめちゃくちゃ大変になるから

直接書き換えるスタイルに慣れてしまうと、

  • 「どこで書き換わったのか」がわかりづらくて
  • バグを追いかけるのが地獄になります…😇

「Stateは必ずコピーを作ってからさわる」 というルールを守っておくと、

  • 「Stateを変える場所」が setState にほぼ集まるので
  • バグの原因を見つけやすくなります 🔍

6️⃣ 「これ使うときは要注意!」メソッド一覧 💣

Stateを扱うときに直接書き換え(破壊的変更)しがちなメソッドたちです。

やばめメソッド 😈何をする?Stateに使うとき
push配列の末尾に追加(元の配列を変更)❌ 使わないようにする
pop最後を削除(元の配列を変更)
shift先頭を削除
unshift先頭に追加
splice途中を削除・追加
sort並び替え(元の配列を変更)
reverse逆順にする

代わりに、こちらを積極的に使うと安全です 🕊

安全メソッド 👼よくある使い方
map配列の各要素から、新しい配列を作る
filter条件に合うものだけを残した新しい配列
slice一部を切り出した新しい配列
スプレッド ...配列・オブジェクトのコピーを作る

7️⃣ ミニ演習:これはOK?NG?書き直してみよう ✍️

🔸 演習1:ユーザーの名前の変更

次のコードは NG な書き方です。 どこが良くないか考えてから、「OKな書き方」に直してみましょう✨

const [user, setUser] = useState({
name: "Yui",
age: 20,
});

const handleChangeName = () => {
// ❌ NG
user.name = "Miku";
setUser(user);
};

👉 ヒント:

  • setUser((prev) => ({ ...prev, ??? })) の形を使う
  • name だけ変えたいので、name: "..." を上書きする

🔸 演習2:TODOを完了済みにする

Tododone フラグがあるとします。

type Todo = {
id: number;
title: string;
done: boolean;
};

const [todos, setTodos] = useState<Todo[]>([]);

const handleDone = (id: number) => {
// ❌ NG
const todo = todos.find((t) => t.id === id);
if (todo) {
todo.done = true;
}
setTodos(todos);
};

これも、

  • find で見つけた todo を直接いじっているのでNG 🙅‍♀️
  • map を使って、新しい配列を作る書き方に直せるとグッドです 💮

👉 たとえばこんな形を目指してみてください(自分で書いてみてから答えを見るのがおすすめ):

  • setTodos((prevTodos) => prevTodos.map(/* ここでidが同じものだけ書き換える */));

8️⃣ まとめ:今日のチェックポイント 🌈

この章で覚えてほしいのはこれだけ 👇

  • State(オブジェクト・配列)は「元のものを直接さわらない」

  • ✅ 代わりに、

    • オブジェクト → ...prev でコピーしてから一部だけ上書き
    • 配列 → map, filter, slice, ...新しい配列を作る
  • push / splice / sort など 元の配列を変えちゃうメソッドは使わない

  • setState((prev) => { ... }) という「前の値から新しい値を作る」形が鉄板パターン


🎓 次の章へのつながり

この「元のデータを直接変えちゃダメ」ルールは、

  • 第46章・47章あたりのオブジェクトState
  • 第48〜50章あたりの配列State
  • 第139〜140章の useImmer を使った書き方

などなど、いろんなところで何度も出てきます。

今のうちに、

「Stateはコピーしてからいじる」= Reactの礼儀作法

として、頭のどこかに貼っておいてくださいね 💕

おつかれさま〜!次の章では、このルールを守りながら フォーム入力の値を安全に扱っていきます ✨