generated at
FC時代に気にかけること
tryangle勉強会(2020/2/28)の資料ですmrsekut
Reactで、classではなくFCでコンポーネントを書いていくときに気にかけるポイント


アジェンダ
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
例題を考えてみる


結論
以下の2つを理解していればいい
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
この発表の趣旨は、
「頑張って再描画を抑えよう」というものではなく、
FCの再描画のタイミングを理解しておこう、というものです


FCはどのタイミングで再描画されるか
結論
自分が持っているstateが変化したとき
親から渡ってくるpropsが変化したとき
親が再描画されたとき
mount時なども描画はされるが、ではないmrsekut


一つずつ簡単な例を確認しよう


自分が持っているstateが変化したときの確認
自分は P
ボタンをクリックすると、stateが更新され、 P が再描画される
ts
const P: React.FC = () => { console.log("再描画!!"); const [cnt, setCnt] = useState(0); return ( <div> <p>{cnt}</p> <button type="button" onClick={() => setCnt(cnt => cnt + 1)}> + </button> </div> ); };


親から渡ってくるpropsが変化したときの確認
自分は C
C からすると、親から来る cnt propsが変化するので再描画される
以下の例は微妙で、適切な例はReduxHooksを使っていないときのContainer Component
propsが変わってなくても親が再描画するので C は再描画される
ts
const P: React.FC = () => { const [cnt, setCnt] = useState(0); 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 は再描画される
ts
const P: React.FC = () => { const [cnt, setCnt] = useState(0); 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
真似したらだめ


反例1: 時間差で値を変える例
以下のコードのstateは再描画させるために用意してあるmrsekut
ボタンをクリックして再描画させた3秒後に値は変わりそうだが?
ts
const P: React.FC = () => { const [cnt, setCnt] = useState(0); 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が変わったから再描画
ts
const P: React.FC = () => { const [cnt, setCnt] = useState(0); const size = window.innerWidth; return ( <div> <div>size: {size}</div> // windowの横幅を変えたときに値は変わる? <button onClick={() => setCnt(cnt => cnt + 1)}>+</button> </div> ); };


反例3: refを使った例
ref.current の中身が変わった場合
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内で定義した関数のこと
ts
const Hoge = () => { const handler = () => {} // ←こいつ return (<></>) }




methodの生成もされる
以下のコードで確認してみよう
関数をglobalな変数に入れて、前回の描画時のものと === 比較してみる
結果が false なら、新しいmethodが生成されているということ
実際、以下のコードでは常に false が返される
ts
let prevFn: any = null; const P: React.FC = () => { const [isOpen, set] = useState(false); 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で対策する
ts
const method1 = useCallback(() => "method 1", []);
true ということは、新しく生成されていないということ





値を返す全てのmethodが再実行される
以下の様な計算は再描画のたびに実行される
再生成もされている
例えば someFunc2() が重い処理の場合どうなる?
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を使って対策
ts
const P: React.FC<{ str: string }> = ({ str }) => { const newStr = useMemo( () => str .split("\n") .map(s => someFunc(s)) .someFunc2() .someFunc3(id), [str, id] ); return <div>{newStr}</div>; };
str に変化がない限りは、再描画されても newStr を再計算しない
実際、上のコード例では諸々の someFunc が純粋関数なら、
引数である str のみで newStr が決まるので、 str に変化がない限りわざわざ再計算する必要がない


ちなみにReact.memoは?
これはhooksではない
親から渡ってきた0個以上のpropsを、前回の描画時のものと比較して再描画するかどうかを決める
第2引数を省略すると、任意のpropsに対して浅い比較のみを行い
第2引数では任意のpropsに対して任意の比較ができる
React.memoに書いた



例題
以下の様なuseCallbackとReact.memoを使っている例を考える
ここで、useCallbackとReact.memoの片方もしくは両方を書かなかったときに、
C が再描画するかどうかを確認してみよう
ts
const P: React.FC = () => { const [count, setCount] = useState(0); 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> ); } );
この4パターンある
memoを使うmemoを使わない
useCallbackを使うOX(特にここ)①
useCallbackを使わないX(特にここ)②X
なんでXになるかわかる?
Xは「無駄に再描画されるよ」という意味

①について
useCallbackのみを使った場合
useCallbackを使っただけでは再描画は止められない
子に何も渡してなくても、親が再描画すれば子は全て再描画する
ts
const P = () => { const [count, setCount] = useState(0); 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は開発時は常にこれをONにしている
再描画された理由を特定するためのhooks
最近v4が出たらしい ref



まとめと所感
以下の2つを理解していればいい
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
mrsekut的にはできるだけFC自体にロジックを書きたくない(きもち)
再利用できそうなものは、積極的にhooksに切り出す
無理なときやそれほどでもないときはFC自体に書く
そのときに上に書いたようなことを気をつける
やるかどうかはさておき
この発表の趣旨は、
「頑張って再描画を抑えよう」というものではなく、
FCの描画ロジックを理解しておこう、というものです
とはいえ、この「再描画の抑制」がどれほどパフォーマンスの改善になるのかは計測できていないmrsekut
ブラウザのJSエンジンは優秀なのであまり頑張る必要もない気がするが
ReactNativeではどうなんだろう、計測してみよう
Elmでの再描画ロジックはどうなんだろう


参考
moyaminさん、thx!!
各種リンク先のリンク