FC時代に気にかけること
一応2/27に社内で発表するつもりの資料
Reactで、classではなくFCでコンポーネントを書いていくときに気にかけるポイント 雑なイラストがあると理解しやすいかも?mrsekut.icon
「再レンダリング」長いので「再描画」にするmrsekut.icon
以下の2つを理解していればいい
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
構造を理解してもらうために、最初の例だけライブコーディングしても良いかも
それ以降は、「できたものはこちらです」をやる
オンラインのどこかにアップしとくか、checkoutするコマンドを資料上に併記しとけば親切
FCはどのタイミングで再描画されるか
結論
渡ってくるpropsが変化したとき
props=値ではない
持っているstateが変化したとき
親が再描画されたとき
ほかもある?
mount時なども描画はされるが、再ではないmrsekut.icon
反例
propsやstate以外の変化の場合は再レンダリングされない
つまり画面は変わらない
良くない例だよmrsekut.icon
時間差で値を変える例
code:ts
const WishState: React.FC = () => {
console.log("with state rendering");
const method2 = () => {
let a;
setTimeout(() => {
a = "time out";
}, 3000);
return a;
};
return (
<div>
<p>{isOpen ? "open" : "close"}</p>
<div>{method2()}</div> // 3秒後に値は変わる?
<button onClick={() => set(!isOpen)}>button</button>
<WithProp isOpen={isOpen} />
</div>
);
};
画面幅の変化によって変える例
code:ts
const WishState: React.FC = () => {
console.log("with state rendering");
const size = window.innerWidth;
return (
<div>
<p>{isOpen ? "open" : "close"}</p>
<div>size: {size}</div> // windowの横幅を変えたときに値は変わる?
<button onClick={() => set(!isOpen)}>button</button>
<WithProp isOpen={isOpen} />
</div>
);
};
確認する
ここの変化しない例としてuseRefも使うといいかも?miyamonz.icon
React詳しくない人に急にrefとかは出さないほうがいいかもというのはある
なるほどです!!mrsekut.icon
だがRefオブジェクトのcurrentプロパティの場合〜〜〜〜
らへんのいめーじですかね
そうですねmiyamonz.icon
refオブジェクトのcurrentの変更は監視されてない
当然useEffect(()=>{/**/}, [ref.current])のように自分でdepsに指定して監視することはできる
FCが再描画されたときに何が起こっているか
以下で「method」と呼んでいるのは、FC内で定義した関数のこと
code:ts
const Hoge = () => {
const handler = () => {} // こいつ
return (<></>)
}
結論
値を返す全てのmethodが再実行される
methodの中の処理の重さは関係ある
対策→useMemo
ここ違くない?methodの中身は呼び出さないと実行されないのでは?miyamonz.icon
再実行される場合、って意味?
そうです!
特にイベントハンドラ系ではなく、表示するための値を計算する系のものを指していました
見出しの部分の書き方が悪かったですmrsekut.icon
code:ts
const Hoge = ({arr}) => {
const newArr = arr.map(a=>a.index * 10) // こういう値を返すやつ
return (
<>
{newArr.map(a => <p>{a.name}</p>)} // 例が微妙(ここで上のmapもしろよってなるので)
</>
)
}
methodの生成もされる
どうやって確認すればいい
methodの中の処理の重さは関係ない
対策→useCallback
hooksも再実行する
よね?
useEffectも再実行される
ただdepsでごにょごにょする
確認する
methodを定義すること自体はそんなにコストではないのかmrsekut.icon
毎回新しいmethodが作られるのは仕方ないのか?
それ自体が問題なのではなく、それが子に遷移すると子が再描画するのが問題
実行するのはもちろんコスト
methodの生成もされる、について
ボタンを押したときに走るものなどについての話
いわゆるイベントハンドラ
前提として、arrow式は常に新規関数オブジェクトを作る
ここがふわっとしてるので視覚化したいmrsekut.icon #?? つまり全く同じ値を返すmethodなのに、毎回新しい関数が作られる
コレ自体は問題ないが、これを子に渡していると子が?再レンダリングされてしまう
別物だと判断するので
code:ts
const calc = () => {}
<Childe calc={calc} />
これまじ?気をつけないとなmiyamonz.icon
コンポーネントにprop渡すところもdepsと同じならそりゃそうか
ちなみに普通のオブジェクトも新しく生成される
code:ts
const a = {}
どうやって確認すればいい
関数オブジェクトってなに?
こんな構文あったんだ、キモ
code:js
// '文字列'で、argsとbodyを指定して関数を生成
const sum = new Function('a', 'b', 'return a + b');
この辺は別に、今回のこととは関係ないか
よくわからんが、() => {}が評価されたら新しい関数オブジェクトが生成されるってことやな
これを視覚化したい
どうやって確認できる?
jsってオブジェクトのユニークid取る方法ないんだよねmiyamonz.icon
自分で作ることはできるがやや大げさ
うわすごい!!!ありがとうございます!見事に可視化できたmrsekut.icon*2
下のやつを書いてから思ったがSymbolでもできそうだな
ムリか
Objectに独自attribute生やすのがなんか説明としてはオーバースペックな気がしていたけどいいかmiyamonz.icon
素直に外側の変数に前回の値を保持して===で比較するほうが単純な気がする
code:js
let prevFn = null
function SomeComponent() {
const someFunc = () => {}
//このままだと常にfalseだし、someFuncをメモ化するとtrueになるはず
console.log(someFunc === prevFn)
prevFn = someFunc
return <>hogehoge</>
}
結局memo化がやってるのってこういうことだし
ほんまだーーーーmrsekut.icon*2
こっちのほうがシンプルで良いですね、ありがとうございます!
再描画時に関数オブジェクトが新しくなることを確認する方法
code:ts
const check = function() {
// @ts-ignore
if (typeof Object.id == "undefined") {
var id = 0;
// @ts-ignore
Object.id = function(o) {
if (typeof o.__uniqueid == "undefined") {
Object.defineProperty(o, "__uniqueid", {
value: ++id,
enumerable: false,
writable: false
});
}
return o.__uniqueid;
};
}
};
check();
const a = { a: 1, b: 2 };
const b = { a: 1, b: 2 };
const c = b;
// @ts-ignore
console.log(Object.id(a)); // 1
// @ts-ignore
console.log(Object.id(b)); // 2 ←異なるものならidがincされる
// @ts-ignore
console.log(Object.id(c)); // 2←同一なので同じ
コレを使う
useCallbackを使わない場合
code:ts
import React, { useState } from "react";
const App = () => <P />;
export default App;
const P: React.FC = () => {
const method1 = () => "method 1";
// @ts-ignore
console.log(Object.id(method1));
return (
<div>
<div>{method1()}</div>
<button onClick={() => set(!isOpen)}>button</button>
</div>
);
};
const check = function() {
// @ts-ignore
if (typeof Object.id == "undefined") {
var id = 0;
// @ts-ignore
Object.id = function(o) {
if (typeof o.__uniqueid == "undefined") {
Object.defineProperty(o, "__uniqueid", {
value: ++id,
enumerable: false,
writable: false
});
}
return o.__uniqueid;
};
}
};
check(); // 最初に実行される
ボタンをクリックすると、stateが更新されるのでPが再描画する
すると上に書いていたとおり関数オブジェクトは新しく作られるので、idがincされていく
https://gyazo.com/3610a359a286553ada6470079c32e0db
useCallbackを使う場合
いかに書き換える
code:ts
const method1 = useCallback(() => "method 1", []);
https://gyazo.com/f0d0efc79baf2aaead976184ae0da29e
↓を考えたときに
code:ts
export const UseCallbackSample = () => {
const updateCount = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<p>
<b>{count}</b>
</p>
<IncrementButton updateCount={updateCount} />
</div>
);
};
const IncrementButton: React.FC<{ updateCount: () => void }> = memo(
({ updateCount }) => {
console.log("再描画してんぞ!!!");
return (
<p>
<button onClick={updateCount}>+</button>
</p>
);
}
);
table:この4パターンある
memoを使う memoを使わない
useCallbackを使う O X(特にここ)①
useCallbackを使わない X(特にここ)② X
なんでXになるかわかる?
Xは「無駄に再描画されるよ」という意味
①について
そもそもこんな感じでも再レンダリングされる
つまり、子に何も渡してなくても、親が再描画すれば子は全て再描画する
code:ts
export const UseCallbackSample = () => {
const updateCount = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<p>
<b>{count}</b>
</p>
<button onClick={updateCount}>+</button>
<IncrementButton2 /> // 何も渡していない
</div>
);
};
const IncrementButton2: React.FC = () => {
console.log("再描画してんぞ!!!");
return <p>ooo</p>;
};
これはIncrementButton2をmemoで囲うことで解消される
memoって「渡ってきたpropsの浅い比較」ではなく、「前のIncrementButton2と、今のIncrementButton2の浅い比較」ってことだねmrsekut.icon
そうなん?
②について
親が再描画したときに、updateCountが新しく生成されるので、子が「新しいものが来た」と判断して再描画する
副作用ある系のものは、hooksにして切り出すのはわかった
つまりコンポーネント内ではなく別ファイルでhooksを管理する
じゃあ副作用のない普通のロジックは?
今まではコンポーネント内に書いてたけど、これも別ファイルに書いてもいいの?
パフォーマンス的に、再描画されてしまったりしない?
useMemoなど何かしら工夫が必要だったりする?
例えば、普通のHogePageに遷移する処理とか、アラート出す処理とか
切り出してらきりがなさそうだが。
管理するの大変そうだけどどうなんだろう
重複がアレば切り出すとか?いけるのか??
コンポーネントを細かく切り出すモチベーションはあるのか?
関数が再実行された時、関数まるごと再計算されるのか
例えば一つのコンポーネントの中にどでかいJSXを書いたとして、
そのJSXの一部で使われているstateが更新された時、
他の部分も再描画されるのか
code:tsx
return (
<div>
でかいJSX
でかいJSX
でかいJSX
でかいJSX
<p>{state}</p> ここだけであるstateを使っている
</div>
)
されるよmrsekut.icon
めっちゃ極端な話、Reactアプリケーション全体を一つのFCの中で書いてしまったら、どんな弊害がある?
もうちょっと小さく、1ページ全体を一つのFCで書いてしまったら、でもいい
JSXもロジック部分もごちゃごちゃするので見づらい
文字通り最低の再利用性
パフォーマンス的にはどう?
ここが気になる。
画面の一部のstateが変更されたら、どこが再描画される?
ページ全体?
code:ts
// とても大きいコンポーネント
const VeryBig: React.FC = () => {
console.log("with state rendering");
const method1 = () => "method 1";
const method2 = () => "method 2"; // 重い処理
const method3 = () => "method 3";
// ..
const method100 = () => "method 100";
return (
<div>
<p>{isOpen ? "open" : "close"}</p>
<div>{method1()}</div>
<div>{method2()}</div>
<div>{method3()}</div>
{/* .. */}
<div>{method100()}</div>
<button onClick={() => set(!isOpen)}>button</button> // ←ここをクリックすると、VeryBigの全てが再計算&再描画される
{/* ↓親であるVeryBigが再描画されたのでこの子らも全て再描画 */}
<Child1 />
<Child2 />
<Child3 />
</div>
);
};
切り出すとどうなるか?
切り出し方にもいくつかある
①propsを親で作って子に流すもの
memoしていなければ、親の再描画で、再描画される
memoに加えて、useMemoやuseCallbackも必要
②propsを流さない切り出し
memoしていなければ、親の再描画で、再描画される
memoしていれば、子は無駄に再描画しない
つまり、切り出すモチベーションが生まれる
できるだけ②でやったほうが、useCallbackを書かなくて良いので楽
つまり切り出すならmemoは必須???
でかい親から、細かい兄弟の子供にすると、
依然として親が再描画すれば、子供は全員再描画だが、
細かい兄弟の一人だけの更新のときは、他の兄弟に影響しない
なので、memoやuseCallbackなどをしなくても、コンポーネントを分割することでパフォーマンスの一助にはなる
何も考えずにmemoやuseMemo、useCallbackを使いまくったときに起こる弊害を考える
「メモ化した値との比較」v.s.「メモ化せずに新しく生成する」の対立構造になる
参考
こういう思考になってしまう
defaultでmemoれば良いじゃん
じゃあ全部React.memoすればよくね?
useMemoとuseCallbackはデフォルトで良いじゃん?
これが、そうでもないんだよなー、という反論
todo
パフォーマンスに関することは、遅さが問題になってきたらやる という基準で良いと思うmiyamonz.icon
最初はuseCallbackとか、子要素のメモ化とか一切やらなくてもそもそもjavascriptが速いので問題にならない
ソレハソウmrsekut.icon
だが、RNではAndroidが激重になる問題がある
そうなのか…miyamonz.icon
計測する方法
dev toolのやつを見る
useWhyDidYouUpdateを使う
今回はもしかしたら関係ないかも
参考
react公式のreconciliationの解説
ここに再描画の方法詳しく書いてある
親が変化したら子も再描画されるとか
そもそも仮想DOMの比較はヒューリスティックな方法であるということとか
だからこそ高速だし、
また完璧でないからこそライブラリユーザがちゃんとメモ化で依存を伝える必要があるといえる
読んだこと合ったのにReact.memoちゃんと知らなかったな…miyamonz.icon
ぶっちゃけ普段遣いでパフォーマンス気にしたことなかったから調べるきっかけなかったな