はじめに
Reactでは、イミュータブルな操作を必要とされることが多いですが、そもそもミュータブル・イミュータブルについていまいち理解していなかったのでまとめます。
Javascriptのデータ型
Javascriptのデータ型はprimitiveとobjectがあります。
primitiveはイミュータブルで、objectはミュータブルです。
ミュータブルは変更可能を意味し、イミュータブルは変更不可能を意味します。
primitiveの場合はイミュータブルであるため、下記のようにコピーをしてその値を変更してもコピー元に対して影響を与えることはありません。
let a = "hello"
let b = a
b = "hola"
console.log(a) // => "hello"
console.log(b) // => "hola"
反対にobjectの場合はミュータブルであるため、コピー元にまで影響が出てしまいます。
const objectOne = {
first_name: "hello",
last_name: "world"
}
const objectTwo = objectOne
objectTwo.first_name = "hola"
console.log(objectOne) // => {first_name: "hola", last_name: "world"}
console.log(objectTwo) // => {first_name: "hola", last_name: "world"}
Reactでイミュータブルな操作をするためには、変数もしくは定数にprimitiveを代入することと、objectを代入することの違いを理解する必要があります。
オブジェクトのコピー
const objectOne = {
first_name: "hello",
last_name: "world"
}
const objectTwo = objectOne
objectTwo.first_name = "hola"
console.log(objectOne) // => {first_name: "hola", last_name: "world"}
console.log(objectTwo) // => {first_name: "hola", last_name: "world"}
上記の例のようにオブジェクトのコピーをした状態を図に表すと、以下のようになります。
objectOneとobjectTwoは同じオブジェクトを参照している状態であるため、ミュータブルな操作になります。
新しいオブジェクトを作成してコピーする
const objectOne = {
first_name: "hello",
last_name: "world"
}
const objectTwo = {...objectOne}
objectTwo.first_name = "hola"
console.log(objectOne) // => {first_name: "hello", last_name: "world"}
console.log(objectTwo) // => {first_name: "hola", last_name: "world"}
上記のようにスプレッドオペレーターを用い、新しいオブジェクトを作成してからコピーした際の状態を図で表すと以下のようになります。この場合は、それぞれが異なるオブジェクトを参照しているため、イミュータブルな操作になります。
objectTwo.first_name = "hola"のように変更した際の状態は下記のようになります。
一方に変更を加えても他方に影響を与えることはありません。
2次元以上でイミュータブルな操作をする
ReactでTodoリストを作るときなどは、以下のような要素がオブジェクトの配列をstateとして持つことが多いと思います。
オブジェクトがネストしている状態なので、ミュータブルな操作をしてしまいバグらせることが多いです。
[
{
"id": "number",
"content": "string"
}
]
const todo = [
{
id: 1,
content: "content1",
},
{
id: 2,
content: "content2",
},
{
id: 3,
content: "content3",
}
]
具体例として上記のような配列で、最初の要素のcontentを変更する場合を考えます。
const copyTodo = todo.slice()
copyTodo[0].content = "content4"
console.log(todo)
// => [{id: 1, content: 'content4'}, {id: 2, content: 'content2'}, {id: 3, content: 'content3'}
console.log(copyTodo)
// => [{id: 1, content: 'content4'}, {id: 2, content: 'content2'}, {id: 3, content: 'content3'}
sliceを使ってコピーした配列に変更を加えた場合、コピー元にも変更が及んでしまっています。これではミュータブルな操作です。
このときの状態を図に表すと以下のようになります。
イミュータブルな操作にするには下記のような状態にしたいです。
以上を踏まえると、イミュータブルな操作にするには下記のようなコードになります。
const copyTodo = todo.map((preObject) => {
return {...preObject}
})
copyTodo[0].content = "content4"
console.log(todo)
// => [{id: 1, content: 'content1'}, {id: 2, content: 'content2'}, {id: 3, content: 'content3'}
console.log(copyTodo)
// => [{id: 1, content: 'content4'}, {id: 2, content: 'content2'}, {id: 3, content: 'content3'}
なぜイミュータブルである必要があるのか
Immutable.js allows us to detect changes in JavaScript objects/arrays without resorting to the inefficiencies of deep equality checks, which in turn allows React to avoid expensive re-render operations when they are not required. This means Immutable.js performance tends to be good in most scenarios.
参考にしたサイト