FC時代に気にかけること
Reactで、classではなくFCでコンポーネントを書いていくときに気にかけるポイント
アジェンダ
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
例題を考えてみる
結論
以下の2つを理解していればいい
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか
この発表の趣旨は、
「頑張って再描画を抑えよう」というものではなく、
FCの再描画のタイミングを理解しておこう、というものです
FCはどのタイミングで再描画されるか
結論
自分が持っているstateが変化したとき
親から渡ってくるpropsが変化したとき
親が再描画されたとき
mount時なども描画はされるが、
再ではない

一つずつ簡単な例を確認しよう
自分が持っているstateが変化したときの確認
自分は P
ボタンをクリックすると、stateが更新され、 P
が再描画される
tsconst 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
は再描画される
tsconst 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
は再描画される
tsconst 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以外の変化の場合は再描画されない
つまり画面は見た目も何も全て変わらない
反例の具体例を紹介
つまり、良くない例だよ

真似したらだめ
反例1: 時間差で値を変える例
以下のコードのstateは再描画させるために用意してある

ボタンをクリックして再描画させた3秒後に値は変わりそうだが?
tsconst 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が変わったから再描画
tsconst 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
の中身が変わった場合
tsconst 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内で定義した関数のこと
tsconst Hoge = () => {
const handler = () => {} // ←こいつ
return (<></>)
}
methodの生成もされる
以下のコードで確認してみよう
関数をglobalな変数に入れて、前回の描画時のものと ===
比較してみる
結果が false
なら、新しいmethodが生成されているということ
実際、以下のコードでは常に false
が返される
tslet 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()
が重い処理の場合どうなる?
tsconst P: React.FC<Hoge> = ({ str, id }) => {
const newStr = str // 引数を
.split("\n") // このへんで
.map(s => someFunc(s)) // ごにょごにょ
.someFunc2() // 計算して
.someFunc3(id); // その結果を
return <div>{newStr}</div>; // 表示
};
useMemoというhooksを使って対策
tsconst 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
に変化がない限りわざわざ再計算する必要がない
これはhooksではない
親から渡ってきた0個以上のpropsを、前回の描画時のものと比較して再描画するかどうかを決める
第2引数を省略すると、任意のpropsに対して浅い比較のみを行い
第2引数では任意のpropsに対して任意の比較ができる
例題
以下の様なuseCallbackとReact.memoを使っている例を考える
ここで、useCallbackとReact.memoの片方もしくは両方を書かなかったときに、
C
が再描画するかどうかを確認してみよう
tsconst 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を使う | O | X(特にここ)① |
useCallbackを使わない | X(特にここ)② | X |
なんでXになるかわかる?
Xは「無駄に再描画されるよ」という意味
①について
useCallbackのみを使った場合
useCallbackを使っただけでは再描画は止められない
子に何も渡してなくても、親が再描画すれば子は全て再描画する
tsconst 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」にチェックを入れる
再描画したところが緑で、再描画が激しいところが黄色で表示される

は開発時は常にこれをONにしている
再描画された理由を特定するためのhooks
まとめと所感
以下の2つを理解していればいい
FCはどのタイミングで再描画されるか
FCが再描画されたときに何が起こっているか

的にはできるだけFC自体にロジックを書きたくない(きもち)
再利用できそうなものは、積極的にhooksに切り出す
無理なときやそれほどでもないときはFC自体に書く
そのときに上に書いたようなことを気をつける
やるかどうかはさておき
この発表の趣旨は、
「頑張って再描画を抑えよう」というものではなく、
FCの描画ロジックを理解しておこう、というものです
とはいえ、この「再描画の抑制」がどれほどパフォーマンスの改善になるのかは計測できていない

ブラウザのJSエンジンは優秀なのであまり頑張る必要もない気がするが
ReactNativeではどうなんだろう、計測してみよう
Elmでの再描画ロジックはどうなんだろう
参考
moyaminさん、thx!!
各種リンク先のリンク