第39章:「元のデータを直接変えちゃダメ」っていう大事なルール
この章では、Reactの超・超・超大事な約束ごとをやります。 ここをちゃんと理解しておくと、
- 画面が「え、なんで今変わった?」ってバグが減る ✨
- 後の「配列のState」「オブジェクトのState」もスルスルわかる 💡
useReducerやuseImmerを使うときも理解が深まる 🧠
という、かなりおいしい内容です。
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」を安全に持っておきたいから
あとから出てくる
useReduceruseImmer- ログ・デバッグ・「戻る」機能 など
では、
それぞれのタイミングの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を完了済みにする
Todo に done フラグがあるとします。
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の礼儀作法
として、頭のどこかに貼っておいてくださいね 💕
おつかれさま〜!次の章では、このルールを守りながら フォーム入力の値を安全に扱っていきます ✨