はじめに
業務で本格的にReactを触り始めて10ヶ月程経過しました。
実装を進める中でドキュメントを読んだり、ググったりする中で必ず目にするのが
- 「Reactはイミュータブルな操作を要請する」
- 「Reactでは破壊的な変更を伴う操作を行ってはいけない]
といった内容のものです。
なんとなくの理解で通り過ぎてしまっていたのですが、
そのせいでコンポーネントが期待どおり再レンダリングされず、画面が変化しないという現象に何度かぶち当たりました。
10ヶ月も基本的なことをスルーしていたので、調べて理解したことをまとめてみました。
ミュータブル/イミュータブルとは
まずは言葉の意味から
- mutable (ミュータブル): 可変
- immutable (イミュータブル): 不変
ミュータブルな操作とは
元の状態の変更を伴う(可変)操作
イミュータブルな操作とは
元の状態の変更を伴わない(不変)操作
言葉だけだとつまりどう言うこと??となるので実際の配列操作をしてみます。
ミュータブルな操作/ミュータブルな操作
配列への要素の追加をしてみます。
const arr = ['1', '2', '3'];
// ミュータブルな操作(イミュータブルではない操作)
arr.push('4');
console.log(arr);
// [ '1', '2', '3', '4' ];
// イミュータブルな操作(ミュータブルではない操作)
const immutableArr = [...arr, '5'];
console.log(immutableArr);
// [ '1', '2', '3', '4', '5' ]
実行結果だけを見てもパッと見違いがありません。
配列の操作で4と5という要素を加えた新しい配列が返ってきてるように見えます。
ここで注目するのが元の配列と新しい配列のデータの参照先です。
arr.push('4');
とした時にはコピー元のconst arr = ['1', '2', '3'];
も
arr.push('4');
をした新しい配列も
arr = [ '1', '2', '3', '4' ];
という情報を参照します。
``arr = ['1', '2', '3'];```
の一つの情報(メモリ)に直接変更が加わり参照されるようになります。
乱暴な言い方ですが、元のarr = ['1', '2', '3'];
の情報はなくなります。
const immutableArr = [...arr, '5'];
とした場合には
コピー元の配列([ '1', '2', '3', '4' ])
と
新しい配列(immutableArr)
の参照先の情報をそれぞれ別々に持ちます。
arrは[ '1', '2', '3', '4' ]という情報を参照。
immutableArrは[ '1', '2', '3', '4', '5' ]という情報を参照。
※言語は異なりますが、下記サイトの図解がわかりやすかったです。
https://kurochan-note.hatenablog.jp/entry/20110316/1300267023
下記のようなオブジェクトの操作でも同様のことが行われます。
const obj = {name: 'a', type: 'old'};
// ミュータブルな操作(イミュータブルではない操作)
obj.type = 'new';
console.log(obj);
// { name: 'a', type: 'new' }
// イミュータブルな操作(ミュータブルではない操作)
const immutableObj = { ...obj, type: 'new' };
console.log(immutableObj);
// { name: 'a', type: 'new' }
なぜReactではイミュータブルな操作が必要なのか
上記を踏まえて、Reactでイミュータブルな操作が必要とされる理由は、
React がコンポーネントの変化をオブジェクトの 同一性(差分) チェックで検知しているためです。
ミュータブルな操作をしてしまうとコピー元の情報も変更されてしまうため、
変更前と変更後の差分をReactが検知できなくなってしまいます。
一方、イミュータブルな操作では変更前と変更後の情報をそれぞれ参照しているので、
Reactは差分を検知することができます。
先ほどのオブジェクトの操作を例に取ると、
const obj = {name: 'a', type: 'old'};
// イミュータブルな操作(ミュータブルではない操作)
const immutableObj = { ...obj, type: 'new' };
console.log(immutableObj);
// { name: 'a', type: 'new' }
obj !== immutableObj
とした時にReactはコンポーネントに変化があったと検知することができます。
しかしミュータブル(イミュータブルではない)な操作をした下記の場合、
const obj = {name: 'a', type: 'old'};
// ミュータブルな操作(イミュータブルではない操作)
obj.type = 'new';
console.log(obj);
// { name: 'a', type: 'new' }
obj.type = 'new';
とした後のobj
は初めとは異なったtypeの値を持ちますが、
obj !== obj
では変更を検知できないため、Reactは差分に気付けないのです。
この変更の検知の仕組みを持つため、Reactではイミュータブルな操作が必要とされるのです。
なのでReactで配列操作やオブジェクト操作をする際には,
concat()
、Object.assign()
やスプレッド演算子
を用いたイミュータブルな操作をする必要があります。
たどたどしい説明になってしましましたが、最後まで読んで頂きありがとうございました。