ReactのuseStateでハマったのでメモ。
要約
タイトル長いな。
- useStateでオブジェクトの配列を管理する際に思ったように値が更新できなくてハマった
- useStateで配列を更新するときは新しい配列を作らなくてはいけない
- useStateでオブジェクトを更新するときは新しいオブジェクトの配列を作らなくてはいけない
- useStateでオブジェクトの配列を更新するときは新しいオブジェクトの新しい配列を作らなくてはいけない
サンプル
こんなTodoListを作ってchangeを押すとtrue/falseが入れ替わるようにしたい。
ダメな例その1
changeを押しても何も変わらない。
https://codesandbox.io/s/intelligent-joana-u4298?file=/src/App.js
import { useState } from "react";
export default function App() {
let [items, updateItems] = useState([
{ name: "item 1", done: false },
{ name: "item 2", done: true },
{ name: "item 3", done: false }
]);
return (
<div>
<h2>Todo list</h2>
<ul>
{items.map((item, idx) => {
return (
<li key={idx}>
<span>{`${item.name} ${item.done} `}</span>
<button
onClick={() => {
updateItems((oldItems) => {
oldItems[idx].done = !oldItems[idx].done;
return oldItems;
});
}}
>
change
</button>
</li>
);
})}
</ul>
</div>
);
}
ダメな例その2:新しい配列を作ってみる
なるほど、どうやら新しい配列を作らないとReactに変更が伝わらずupdateが走らないらしい。
https://qiita.com/10mi8o/items/896df09ad89e41d48bac
というわけで新しい配列を作ってみる。
https://codesandbox.io/s/agitated-joana-4kj4s?file=/src/App.js
<button
onClick={() => {
updateItems((oldItems) => {
oldItems[idx].done = !oldItems[idx].done;
return [...oldItems]; // 新しい配列
});
}}
>
change
</button>
しかし、最初の1回のみ変わるもあとは動かない...
ダメな例その3:新しいオブジェクトを作ってみる
オブジェクトの方を変更しないといけないのかと思い今度は配列内部のオブジェクトを変更することにする
https://codesandbox.io/s/sweet-visvesvaraya-7yr4s?file=/src/App.js
<button
onClick={() => {
updateItems((oldItems) => {
// 新しいオブジェクトを作成
oldItems[idx] = {
...oldItems[idx],
done: !oldItems[idx].done
};
return oldItems;
});
}}
>
change
</button>
けど動かない。
うまくいった例:新しいオブジェクトを作って新しい配列に突っ込む
これで思った通りに動いた。
<button
onClick={() => {
updateItems((oldItems) => {
return oldItems.map((oldItem, oldIdx) => {
if (oldIdx === idx) {
return { ...oldItem, done: !oldItem.done };
}
return oldItem;
});
});
}}
>
change
</button>
なんでうまくいかなかったのか
React内部ではObject.is
を使って比較しているらしい。
https://ja.reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/is#description
Object.isの結果を表示してみると以下のようになった。
<button
onClick={() => {
updateItems((oldItems) => {
console.log(
"新しい配列の中に古いオブジェクト:",
Object.is(items, [...oldItems]),
Object.is(items[idx], [...oldItems][idx])
); // false, true
oldItems[idx] = {
...oldItems[idx],
done: !oldItems[idx].done
};
console.log(
"古い配列の中に新しいオブジェクト",
Object.is(items, oldItems),
Object.is(items[idx], oldItems[idx])
); // true, true
const newItems = oldItems.map((oldItem, oldIdx) => {
if (oldIdx === idx) {
return { ...oldItem, done: !oldItem.done };
}
return oldItem;
});
console.log(
"新しい配列の中に新しいオブジェクト",
Object.is(items, newItems),
Object.is(items[idx], newItems[idx])
); // false, false
return newItems;
});
}}
>
change
</button>
配列の中のオブジェクトを内部でさらにどう比較しているかまでは調べられてないが、配列自体もオブジェクト自体もObject.isがfalseにならないといけないっぽい。
理解に間違い、より良い書き方などあれば教えてください🙇♂️