Reactのドキュメントを読んでいてshallow copyのことが出てきたので、アウトプットしようと思います。
目次
- shallow copy
- primitive型とshallow copy
- オブジェクト型とshallow copy
- 配列型とshallow copy
shallow copy
documentは以下になります。
オブジェクトのシャローコピーとは、コピーが作成されたソースオブジェクトのプロパティと同じ参照(同じ基礎的な値を指す)を共有するコピーのことです。その結果、ソースまたはコピーのどちらかを変更すると、もう一方のオブジェクトも変更される可能性があります。この動作は、ソースとコピーが完全に独立しているディープコピーの動作とは対照的です。
上記はdeeplの日本語訳です。
ここではObject referenceと同じと説明されていて、Object referenceはオブジェクトのリンクと説明されている。僕の認識はC言語でいうpointerのような認識。
Object referenceについては以下
つまり、shallow copyはC言語でいうpointerでオブジェクトの参照である。
Reactとshallow copy
ここではshallow copyがdocumentで出てきたところを例として説明しようと思います。
primitive型について
jsはprimitive型の値の更新ができないようです。参照は以下
JavaScriptでは、プリミティブ値は不変です。プリミティブ値が一度作成されると、それを保持する変数に別の値が再割り当てされることはあっても、変更することはできません。これとは対照的に、オブジェクトと配列はデフォルトでミュータブルです。
しかしオブジェクトと配列はmutableなのでおって配列については説明させていただきます。
React primitive型とshallow copy
React.dev documentでの該当は以下です。
Updating Objects in Stateということで、stateを更新する際のshallow copyのことについて言及されています。
冒頭以下が書かれてあります。
ステートには、オブジェクトを含め、あらゆる種類のJavaScriptの値を保持できる。しかし、Reactステートに保持するオブジェクトを直接変更してはいけない。オブジェクトを更新したい場合は、新しいオブジェクトを作成し(または既存のオブジェクトのコピーを作成し)、そのコピーを使用するようにステートを設定する必要がある。
つまり、値を変更する際は、新しくオブジェクト(コピー)を作成してstateの設定をするべきということです。
const [position, setPosition] = useState({ x: 0, y: 0 });
上記を以下のように変更した時に
position.x = 5;
しかし、Reactステートのオブジェクトは技術的にはミュータブルだが、数値やブーリアン、文字列のようにイミュータブルであるかのように扱うべきだ。オブジェクトを変異させるのではなく、常に置き換えてください。
少し前に言及したjsのprimitive型における変数の更新できないことを説明されています。
記事ではjs状態変数はread onlyで扱いましょうと言われています。
状態の更新をするときはよく知られているuseStateのset関数を使用して更新することを触れています。
deep diveのエリアで以下のような説明もありました。
position.x = e.clientX;
position.y = e.clientY;
このように変数を直接更新することがよくないとjsのdocsを用いて説明したのですが、以下のケースだと問題ないみたいです。
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);
なぜなら変異が問題になるのは、すでに状態になっている既存のオブジェクトを変更する場合だからだそうです。
変異が問題になるのは、すでに状態になっている既存のオブジェクトを変更する場合だけだ。作成したばかりのオブジェクトを変異させても、他のコードがまだそれを参照していないので問題ない。
とあって、この変数を使用している箇所の依存関係に影響を与えるわけではないのでという感じです。
Immerについて
Immerはmutableなオブジェクト、配列でshallow copyを意識せずに値の更新をすることができます。
2019年Reactオープンソース賞「Breakthrough of the year」、JavaScriptオープンソース賞「Most impactful contribution」受賞。
上記のように信頼性も高いようです。
では早速Immerの説明に入ります。
setPerson({
firstName: e.target.value, // New first name from the input
lastName: person.lastName,
email: person.email
});
上記のような状態更新があったときにspread構文を用いて以下のように記述を変更可能です。
setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});
また、以下のreact docsにある通り、spread構文もshallow copyです。
...スプレッド構文は "シャロー "であることに注意してほしい。このため高速に処理できますが、ネストしたプロパティを更新したい場合、複数回使用しなければならないことを意味します。
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
上記のようなコードがあったときに
person.artwork.cityを変更したいときに以下のように書けないのはこれまで説明した通りです。
person.artwork.city = 'New Delhi';
そこでspread構文を利用して以下のように書けます。
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);
しかし、spread構文を使わないといけないかつ、nestされているのでコードが煩雑になってしまいます。
Immerを使うとオブジェクトをそのまま、shallow copyを考慮しないで状態を変更することが可能です。例としては以下のコード
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
Immerのgithubにあったサンプルコードを以下に参照として貼っておきます。
import React from "react";
import { useImmer } from "use-immer";
function App() {
const [person, updatePerson] = useImmer({
name: "Michel",
age: 33
});
function updateName(name) {
updatePerson(draft => {
draft.name = name;
});
}
function becomeOlder() {
updatePerson(draft => {
draft.age++;
});
}
return (
<div className="App">
<h1>
Hello {person.name} ({person.age})
</h1>
<input
onChange={e => {
updateName(e.target.value);
}}
value={person.name}
/>
<br />
<button onClick={becomeOlder}>Older</button>
</div>
);
}
このようにImmerを使えば簡単にnestされたobjectも編集することが可能です。
Immerはどのようにworkしているのか
proxyを使用してオブジェクト変更を感知、オブジェクト操作を代わりに実行し、値の変更を含む新しいオブジェクトを生成しています。なので、代わりにobject操作をしてくれてshallow copyなどを意識せずに値更新ができるという利点があります。
以下proxy document↓
ReactではどうしてImmutableが推奨されるのか
- Reactの最適化戦略は、前のpropsやステートが次のものと同じであれば作業をスキップすることに依存しているから
- React stateはsnapshotのように扱われ、値の変更と再レンダリングのトリガーになるから
- 過去の状態をメモリに保持していて同じ場面で使える時があれば再利用できるから
React 配列とshallow copy
先ほど触れた通り、jsでの配列はmutableになっています。
なので、primitive型と違い、直接に値を変更することが可能です。
しかし、stateに格納する場合は不変として扱う必要があります。オブジェクトと同じように、stateに格納された配列を更新したい場合は、新しい配列を作成し(または既存の配列をコピーし)、新しい配列を使用するようにstateを設定する必要があります。
以下はReactのdocumentからのスクショになります。
配列においてもImmerが使用可能です。後述したいと思います。
setArtists( // Replace the state
[ // with a new array
...artists, // that contains all the old items
{ id: nextId++, name: name } // and one new item at the end
]
);
arrayもmutableなので、上記の表に乗っ取り、上記のようにshallow copyで書くことでimmutableコーディングが可能です。
サンプルコードでは、spread構文を使用して配列オブジェクトを使用してReplaceしています。
略
function handleToggleYourList(artworkId, nextSeen) {
const yourNextList = [...yourList];
const artwork = yourNextList.find(
a => a.id === artworkId
);
artwork.seen = nextSeen;
setYourList(yourNextList);
}
略
また以下のようにmapを使用して記述することも可能です。
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
ここでImmerを使用すると以下になります。
const [yourList, updateYourList] = useImmer(
initialList
);
function handleToggleMyList(id, nextSeen) {
updateMyList(draft => {
const artwork = draft.find(a =>
a.id === id
);
artwork.seen = nextSeen;
});
}
なのでオブジェクト、配列のstate管理にはImmerを使用するのが良いと思います。
今回はReactのdocumentを読んでいてImmerについて、React, jsのshallow copyについて不明な点がクリアになったのでoutputしてみました。
最後まで読んでいただきありがとうございました。
参照