FC時代に気にかけること
Reactで、classではなくFCでコンポーネントを書いていくときに気にかけるポイント
アジェンダ
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
例題を考えてみる
結論
以下の2つを理解していればいい
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
この発表の趣旨は、
「頑張って再描画を抑えよう」というものではなく、
FCの再描画のタイミングを理解しておこう、というものです
FCはどのタイミングで再描画されるか
結論
自分が持っているstateが変化したとき
親から渡ってくるpropsが変化したとき
親が再描画されたとき
mount時なども描画はされるが、再ではないmrsekut.icon
一つずつ簡単な例を確認しよう
自分が持っているstateが変化したときの確認
自分はP
ボタンをクリックすると、stateが更新され、Pが再描画される
code:ts
const P: React.FC = () => {
console.log("再描画!!");
return (
<div>
<p>{cnt}</p>
<button type="button" onClick={() => setCnt(cnt => cnt + 1)}>
+
</button>
</div>
);
};
親から渡ってくるpropsが変化したときの確認
自分はC
Cからすると、親から来るcntpropsが変化するので再描画される
以下の例は微妙で、適切な例はReduxHooksを使っていないときのContainer Component
propsが変わってなくても親が再描画するのでCは再描画される
code:ts
const P: React.FC = () => {
return (
<div>
<C cnt={cnt} />
<button type="button" onClick={() => setCnt(cnt => cnt + 1)}>
+
</button>
</div>
);
};
const C: React.FC<{ cnt: number }> = ({ cnt }) => {
console.log("再描画!!");
return <p>{cnt}</p>;
};
親が再描画されたときの確認
Cに何も渡してないが、Cは再描画される
code:ts
const P: React.FC = () => {
return (
<div>
<C /> {/* 何も渡してない */}
<p>{cnt}</p>
<button type="button" onClick={() => setCnt(cnt => cnt + 1)}>
+
</button>
</div>
);
};
const C: React.FC = () => {
console.log("再描画!!");
return <p>ccc</p>;
};
反例
propsやstate以外の変化の場合は再描画されない
つまり画面は見た目も何も全て変わらない
反例の具体例を紹介
つまり、良くない例だよmrsekut.icon
真似したらだめ
反例1: 時間差で値を変える例
以下のコードのstateは再描画させるために用意してあるmrsekut.icon
ボタンをクリックして再描画させた3秒後に値は変わりそうだが?
code:ts
const P: React.FC = () => {
let a = "start";
setTimeout(() => {
a = "time out";
}, 3000);
return (
<div>
<div>{returnA()}</div> // 3秒後に値は変わる?
<button onClick={() => setCnt(cnt => cnt + 1)}>+</button>
</div>
);
};
反例2: 画面幅の変化によって変える例
windowの横幅を表示している
windowの横幅を変えると値は変わりそうだが?
横幅を変えたあとに、ボタンをクリックもしくはリロードをすると値は変わる
前者はstateが変わったから再描画
code:ts
const P: React.FC = () => {
const size = window.innerWidth;
return (
<div>
<div>size: {size}</div> // windowの横幅を変えたときに値は変わる?
<button onClick={() => setCnt(cnt => cnt + 1)}>+</button>
</div>
);
};
反例3: refを使った例
ref.currentの中身が変わった場合
code:ts
const P: React.FC = () => {
const ref = useRef(0);
const onClick = () => {
setTimeout(() => {
ref.current += 1; // 時間差でref.currentを変更
}, 1000);
};
return (
<div>
<p>{ref.current}</p> // 変わるのか??
<button type="button" onClick={onClick}>
+
</button>
</div>
);
};
今見たように、再度↓
propsやstate以外の変化の場合は再描画されない
つまり画面は見た目も何も全て変わらない
FCが再描画されたときに何が起こっているか
結論
methodの生成もされる
対策→useCallback
値を返す全てのmethodが(再生成された後に)再実行される
対策→useMemo
このスクボで「method」と呼んでいるのは、FC内で定義した関数のこと
code:ts
const Hoge = () => {
const handler = () => {} // ←こいつ
return (<></>)
}
methodの生成もされる
以下のコードで確認してみよう
関数をglobalな変数に入れて、前回の描画時のものと===比較してみる
結果がfalseなら、新しいmethodが生成されているということ
実際、以下のコードでは常にfalseが返される
code:ts
let prevFn: any = null;
const P: React.FC = () => {
const method = () => "method 1";
console.log(method === prevFn);
prevFn = method;
return (
<div>
<C func={method} />
<button onClick={() => set(!isOpen)}>button</button>
</div>
);
};
const C: React.FC<{ func: () => void }> = ({ func }) => <div>child</div>;
この書き方は、JSXの属性に関数を直書きするのとほぼ同じ
useCallbackで対策する
code:ts
const method1 = useCallback(() => "method 1", []);
trueということは、新しく生成されていないということ
値を返す全てのmethodが再実行される
以下の様な計算は再描画のたびに実行される
再生成もされている
例えばsomeFunc2()が重い処理の場合どうなる?
code:ts
const P: React.FC<Hoge> = ({ str, id }) => {
const newStr = str // 引数を
.split("\n") // このへんで
.map(s => someFunc(s)) // ごにょごにょ
.someFunc2() // 計算して
.someFunc3(id); // その結果を
return <div>{newStr}</div>; // 表示
};
useMemoというhooksを使って対策
code:ts
const P: React.FC<{ str: string }> = ({ str }) => {
const newStr = useMemo(
() => str
.split("\n")
.map(s => someFunc(s))
.someFunc2()
.someFunc3(id),
);
return <div>{newStr}</div>;
};
strに変化がない限りは、再描画されてもnewStrを再計算しない
実際、上のコード例では諸々のsomeFuncが純粋関数なら、
引数であるstrのみでnewStrが決まるので、strに変化がない限りわざわざ再計算する必要がない
これはhooksではない
親から渡ってきた0個以上のpropsを、前回の描画時のものと比較して再描画するかどうかを決める
第2引数を省略すると、任意のpropsに対して浅い比較のみを行い
第2引数では任意のpropsに対して任意の比較ができる
例題
以下の様なuseCallbackとReact.memoを使っている例を考える
ここで、useCallbackとReact.memoの片方もしくは両方を書かなかったときに、
Cが再描画するかどうかを確認してみよう
code:ts
const P: React.FC = () => {
const updateCount = useCallback(() => setCount(count => count + 1), []); // ここ
return (
<div>
<p>{count}</p>
<C updateCount={updateCount} />
</div>
);
};
// ここ
const C: 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は「無駄に再描画されるよ」という意味
①について
useCallbackのみを使った場合
useCallbackを使っただけでは再描画は止められない
子に何も渡してなくても、親が再描画すれば子は全て再描画する
code:ts
const P = () => {
const updateCount = useCallback(() => setCount(count => count + 1), []);
return (
<div>
<p>{count}</p>
<button onClick={updateCount}>+</button>
<C2 /> {/* 何も渡していない */}
</div>
);
};
const C2: React.FC = () => {
console.log("再描画してんぞ!!!");
return <p>ooo</p>;
};
これはC2をmemoで囲うことで解消される
親から渡ってくる0個のpropsを比較して常にtrueなので再描画されない
②について
React.memoのみを使った場合
親が再描画したときに、updateCountがそのたびに新しく生成されるので、子が「新しいものが来た」と判断して再描画する
これはupdateCountが関数オブジェクトなのでそういう挙動になる
updateCountが関数ではなく、値の場合はまた違う挙動になる
これらからなんとなくわかること
propsのバケツリレーが激しいと、その経路は全て再描画される
再描画の伝達の途中にmemoしているコンポーネントがあれば、そこで再描画伝達はストップできる
一つのコンポーネントが超大きいと再描画される確率(?)が上がる
だからといってuseMemoやuseCallback何も考えずに使いまくらない
無意味な例
第二引数がない
参照を渡さないコールバック
無意味な第二引数を指定している
このスライドが長くなってしまったので↓を読んで
再描画を計測する方法
Componentsタブで「Highlight updates when components render」にチェックを入れる
再描画したところが緑で、再描画が激しいところが黄色で表示される
mrsekut.iconは開発時は常にこれをONにしている
再描画された理由を特定するためのhooks
まとめと所感
以下の2つを理解していればいい
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
mrsekut.icon的にはできるだけFC自体にロジックを書きたくない(きもち)
再利用できそうなものは、積極的にhooksに切り出す
無理なときやそれほどでもないときはFC自体に書く
そのときに上に書いたようなことを気をつける
やるかどうかはさておき
この発表の趣旨は、
「頑張って再描画を抑えよう」というものではなく、
FCの描画ロジックを理解しておこう、というものです
とはいえ、この「再描画の抑制」がどれほどパフォーマンスの改善になるのかは計測できていないmrsekut.icon
ブラウザのJSエンジンは優秀なのであまり頑張る必要もない気がするが
ReactNativeではどうなんだろう、計測してみよう
Elmでの再描画ロジックはどうなんだろう
参考
moyaminさん、thx!!
各種リンク先のリンク