今回の記事の内容
- データタイプについて
- コピーのメカニズム
- 参照の同一性
- レンダリングについて
##データタイプ
JSのデータタイプには二種類
####プリミティブ型
メソッドを持たないデータ型
- string
- number
- BigInt
- boolean
- undefined
- symbol
####オブジェクト型
プロパティとメソッドの集まり
- array
- object
- function
- RegExp
## JSのコピーのメカニズム
####プリミティブ型
JSで変数などを宣言したら、その変数はパソコンのメモリに保存される。プリミティブ型の変数はコピーする際に新しいメモリ空間を確保して独立的な値を保存。
let string = "りんご"; //←独立して保存される
let newString = string;
newString = "apple"
stringをコピーしてnewStringという変数を生成。そしてnewStringの値を変更。
console.log(string); //りんご
console.log(newString); // apple
それでも原本のstringには何の影響もない。これは違うメモリに保存されているため。
####オブジェクト型
プリミティブタイプのように新しいメモリに保存するのではなく、原本のメモリアドレスを渡される。つまり、原本とコピー本が同じメモリに保存されている同じデータを共有する。
const object = { name: "apple" };
const newObject = object;
newObject.name = "banana"
console.log(object); // {name: "banana"}
console.log(newObject); // {name: "banana"}
プリミティブ型と同じように変更すると、このようになる
これは同じデータを共有しているからこうなる。
## 参照の同一性
これどうなる?
[] === []
{} === {}
(() => {}) === (() => {})
0 === 0
"string" === "string"
true === true
false === false
//false
[] === []
{} === {}
(() => {}) === (() => {})
//true
0 === 0
"string" === "string"
true === true
false === false
こっから本編
## レンダリングについて
###useCallback
useCallbackはメモ化されたコールバックを返すHook。
依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算する。
####メモ化とは?
コストが高い呼び出しの結果を保存し、同じ入力が再び発生したときにキャッシュされた結果を返すことによってプログラム実行速度を向上させる技術。
公式では
不必要なレンダーを避けるために参照の同一性を見るよう最適化されたコンポーネントにコールバックを渡す場合に便利
と書かれている。
逆に言うたら
「参照の同一性を見るよう最適化されたコンポーネントにコールバックを渡す場合じゃないと別に要らない」
となる。
ここで基礎的な知識として
- Reactコンポーネントは自分のstateが変更されたり、親コンポーネントから渡されるpropsが変更された場合再レンダリングされる。
- コンポーネントが再レンダリングされると、その中で宣言されている関数や変数は以前保存されていたメモリを空けて新しいメモリに再び保存される(garbage collection)。
###これらの知識から分かること
こんなボタンコンポーネントがあったとする
const CountButton = function CountButton({ onClick, count }) {
return <button onClick={onClick}>{count}</button>;
};
function DualCounter() {
const [count1, setCount1] = React.useState(0);
const increment1 = () => setCount1(c => c + 1);
const [count2, setCount2] = React.useState(0);
const increment2 = () => setCount2(c => c + 1);
return (
<>
<CountButton count={count1} onClick={increment1} />
<CountButton count={count2} onClick={increment2} />
</>
);
}
- DualCounterのstateであるcount1が変更される
- DualCounterが再レンダリングされる
- DualCounterの中の変数や関数(count1、setCount1、increment1など)たち全部がもともと保存されてたメモリを空けて新しいメモリに保存される
- CountButtonは引数で渡される変数と関数の変更チェック
- increment1とincrement2がオブジェクト型のため、保存されたメモリが変わったことで新しいやつだと判断
- CountButton両方とも再レンダリングされる。
結局一つのCountButtonを押しただけなのにCountButtonが全部再レンダリングされてしまう。
次
こんなコンポーネントがあったとする
const CountButton = function CountButton({ onClick, count }) {
return <button onClick={onClick}>{count}</button>;
};
function DualCounter() {
const [count1, setCount1] = React.useState(0);
const increment1 = React.useCallback(() => setCount1(c => c + 1), []);
const [count2, setCount2] = React.useState(0);
const increment2 = React.useCallback(() => setCount2(c => c + 1), []);
return (
<>
<CountButton count={count1} onClick={increment1} />
<CountButton count={count2} onClick={increment2} />
</>
);
}
####どうなる?
###まだ全部再レンダリングされる
今のCountButtonたちは渡される引数に変更がなくても親であるDualCounterが再レンダリングされたので自分自身も再レンダリングされた。
これが子コンポーネントを最適化しなければならない理由。
####答えはこう
const CountButton = React.memo(function CountButton({ onClick, count }) {
return <button onClick={onClick}>{count}</button>
})
function DualCounter() {
const [count1, setCount1] = React.useState(0)
const increment1 = React.useCallback(() => setCount1(c => c + 1), [])
const [count2, setCount2] = React.useState(0)
const increment2 = React.useCallback(() => setCount2(c => c + 1), [])
return (
<>
<CountButton count={count1} onClick={increment1} />
<CountButton count={count2} onClick={increment2} />
</>
)
}
React.memoはパフォーマンス最適化のための高階コンポーネント(HOC, higher-order component)で,props の変更のみをチェックする。
高階コンポーネントとは、コンポーネントを引数としてもらって新しいコンポーネントを返す関数。React.memoの場合は引数としてコンポーネントをもらい、最適化されたコンポーネントを返す。
もしあるコンポーネントが同じ props を与えられたときに同じ結果をレンダーするなら、結果を記憶してパフォーマンスを向上させるためにそれを React.memo でラップすることができます。つまり、React はコンポーネントのレンダーをスキップし、最後のレンダー結果を再利用します。
要するにReact.memoでコンポーネントを囲んだら、渡された引数に変化があるかどうかチェックして変化がある場合のみ再レンダリングされる機能が追加される。
##結論
なんでもかんでもuseCallback使えばおkというわけではない
既に上記のCountButtonの例でuseCallbackだけ使ったら子コンポーネントの再レンダリングを防げないことを学びました。useCallbackの目的である「不必要なレンダーを避ける」ができてないのによい使い方のはずがないでしょう。
それに関数宣言はコストが安い処理なので、わざわざuseCallbackまで使って防げるべきものではない。
useCallbackを使うことにもコストがかかる。useCallbackというHookを読み込むし、第2引数で配列の宣言もするし、レンダリングの度にuseCallbackが動きます。useCallbackを使ったほうが得な時もあるが、場合によっては逆に余計なメモリを食う時だってある。
「When to useMemo and useCallback」の著者であるKent C. Doddsはこう言いました。
MOST OF THE TIME YOU SHOULD NOT BOTHER OPTIMIZING UNNECESSARY RERENDERS. React is VERY fast and there are so many things I can think of for you to do with your time that would be better than optimizing things like this.
ほとんどの場合、不要なレンダリングの最適化は気にしなくていいです。Reactは非常に速いし、このようなものを最適化するよりも、他に時間を割いてやるべきことはたくさんあります
そして今まで話した最適化の必要性も極稀と言ってます。彼がPayPalで働いた3年間、そしてそれよりも長いReact歴の間もそいういう最適化が必要な瞬間はなかったようです。
要するにuseCallbackを使うべき瞬間はそんなに多くないということになる。それなのに必要でもないHookを「とりあえず入れとこう」という気持ちで使うことはやめたほうがいい。