Reactでは破壊的メソッドの使用は推奨されていません。
破壊的メソッドとは
破壊的なメソッドとは、オブジェクトや配列などのデータ構造を変更する操作を指します。これらのメソッドは、元のデータ構造を直接変更し、新しいデータ構造を返さないことが特徴です。そのため、元のデータが変更される可能性があります。
破壊的なメソッド: push()、pop()、shift()、unshift()、splice()など
非破壊的メソッドとは
非破壊的なメソッドは、元のデータ構造を変更せず、新しいデータ構造を返す操作です。元のデータは変更されず、変更されたコピーが返されます。
非破壊的なメソッド: slice()、concat()、map()、filter()など
破壊的メソッドを使ってはいけない理由
Reactはコンポーネントの再レンダリングを行う際に、参照の変更を検知して効率的な更新をいます。
破壊的メソッドが推奨されない理由は、破壊的なメソッドは元のデータ構造を変更するため、参照が変更されず、Reactが再レンダリングの必要性を検知できなくなるためです。
具体的な例
下記は破壊的メソッドを使用した例です。
export default function Sample() {
const [state, setState] = useState(["a", "b", "c"]);
const handleButtonClick = () => {
const newState = state;
newState.push("d"); // 破壊的なメソッドを使って state を更新するが、state 自体は直接変更されない
console.log("New state:", newState);
console.log("Original state:", state);
setState(newState); // 更新を反映させるが、state は変更されていない
};
return (
<div>
<button onClick={handleButtonClick}>Add 'd' to state</button>
<ul>
{state.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
挙動
問題点
- ボタンを押しても"d"が画面上に表示されない
更新されない理由とReactの仕組みについて
このコードの問題点は、const newState = state;
で、newState
をstate
のコピーではなく、同じ配列を参照している点です。
そのため、console.logで表示されているように、newState
とstate
、両方で"d"が追加されています。
Reactは、コンポーネントの再レンダリングが必要かどうかを判断する際に、状態の変更を検出します。
newState
はstate
と同じ配列を参照しているため、実際にはstate
自体が直接変更されています。
そのため、newState
が呼び出されても、state
の参照が変更されないため、Reactは新しい状態の変更を検出できません。
結果として、状態の更新が反映されず、コンポーネントが再レンダリングされません。
修正
修正するためには、新しい配列を作成して、それに変更を加える必要があります。
export default function Sample() {
const [state, setState] = useState(["a", "b", "c"]);
const handleButtonClick = () => {
const newState = [...state, 4];
console.log("New state:", newState);
console.log("Original state:", state);
setState(newState); // 更新を反映させるが、state は変更されていない
};
return (
<div>
<button onClick={handleButtonClick}>Add 'd' to state</button>
<ul>
{state.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
newState
の作成時に、スプレッド演算子 ... を使ってstate
の内容を展開しています。これにより、新しい配列が作成され、state
の内容は変更されません。
setState
を使ってnewState
をセットすることで、Reactに新しい状態を反映させています。
状態を安全に更新し、Reactが再レンダリングをトリガーするために、元の配列を更新するのではなく新しい配列を作成するようにしましょう。
破壊的メソッドから非破壊的メソッドへ変換する
A・B・Cのボタンがあります。
初期値はconst array = ["あいうえお","かきくけこ","さしすせそ"];
の文字列が入った配列が格納されています。
A
のボタンを押した際は、初期値の配列がそのまま表示されます。
B
のボタンを押した際は、配列の2番目の値("かきくけこ")
が配列の一番最初に移動します。
C
のボタンを押した際は、配列の3番目の値("さしすせそ")
が配列の一番最初に移動します。
他の配列はそのままの順番通りです。
イメージ
破壊的なメソッドを使用したNGパターン
export default function Sample() {
const [array, setArray] = useState([
"あいうえお",
"かきくけこ",
"さしすせそ",
]);
const handleButtonClick = (targetName) => {
const newArray = array.slice(); // 配列をコピーする
const targetIndex = newArray.indexOf(targetName);
if (targetIndex !== -1) {
newArray.splice(targetIndex, 1); // 対象を配列から削除
newArray.unshift(targetName); // 対象を配列の先頭に追加
setArray(newArray); // 新しい並び順を設定
}
};
return (
<>
<button onClick={() => handleButtonClick("あいうえお")}>A</button>
<button onClick={() => handleButtonClick("かきくけこ")}>B</button>
<button onClick={() => handleButtonClick("さしすせそ")}>C</button>
<ul>
{array.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</>
);
}
非破壊的なメソッドを使用したOKパターン
export default function Sample() {
const [array, setArray] = useState([
"あいうえお",
"かきくけこ",
"さしすせそ",
]);
const handleButtonClick = (targetName: string) => {
const targetIndex = array.indexOf(targetName);
if (targetIndex !== -1) {
//
const newArray = [
targetName,
...array.filter((item, index) => index !== targetIndex),
];
setArray(newArray);
}
};
return (
<>
<button onClick={() => handleButtonClick("あいうえお")}>Aのボタン</button>
<button onClick={() => handleButtonClick("かきくけこ")}>Bのボタン</button>
<button onClick={() => handleButtonClick("さしすせそ")}>Cのボタン</button>
<ul>
{array.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</>
);
}
この修正では、splice()
メソッドと unshift()
メソッドの代わりに、新しい配列を作成する方法を採用しています。
具体的には、対象の要素を取り除いた新しい配列をfilter()
メソッドを使って作成し、その配列の先頭に対象の要素を追加しています。これにより、元の配列を破壊することなく、状態の更新が行われます。
スプレッド構文を使うことでミューターブルではなく、イミュータブル(変更不可能な値) なものとして扱われます。
最後に
コンポーネントの状態の管理をより安全で予測可能なものになるように、元のデータを直接変更したり、破壊的メソッドを使用することは避けましょう。
参考
React ドキュメント 「Local mutation: Your component’s little secret」
記事