why
パフォーマンスチューニングのやり方を整理したかった
参考動画
https://www.youtube.com/watch?v=KXhE1tBBfJc
あべちゃんさんの React Hooks の動画
【ReactHooks入門】第6回:useCallbackの理解
を参考にしました。
そもそもなぜ React の useState は値が変わって再描画されるのか
taroro28 さんの Zenn のこの記事に答えがあった
react/packages/react-reconciler/src/ReactFiberHooks.new.js
react ライブラリのここで setState が行われたときに
eagerState, currentState の比較が行われる
それで違う場合に再描画が起こるらしい。
CRA
npx create-react-app pf --template typescript
pf という名前で CRA
Title コンポーネントを作成
https://www.youtube.com/watch?v=KXhE1tBBfJc&t=510s
type Props = { titleText: string }
const Title: React.FC<Props> = ({titleText}) => {
return (
<h2> {titleText} </h2>
);
}
export default Title;
App から Props として受け取る titleText を
そのまま Props という型で定義して
コンポーネントに React.FC にはめて titleText を引数処理して
h2 でラップして 返す処理を書いた。
App で titleText を渡して呼び出す
import Title from './components/Title'
function App() {
const titleText = '#6 useCallback'
return (
<div className="App">
<Title titleText={titleText} />
</div>
);
}
export default App;
これで読み込めた。
Subtitle も同じように作る
A と B のカウンターを div で追加
これらに console.log を仕込んでも最初は一度ずつしか読み込まれない。
useState とか一切ないから。
しかし、ここに状態を持ち込んで変化させる関数を useState で導入すると
問題が表明化する。
function App() {
const [countA, setCountA] = useState<number>(0)
const [countB, setCountB] = useState<number>(0)
const titleText = '#6 useCallback'
const subTitleText = 'アンケート'
return (
<div className="App">
<Title titleText={titleText} />
<SubTitle subTitleText={subTitleText} />
<div>{countA}</div>
<div>{countB}</div>
<button onClick={ () => setCountA(countA+1)}>A に 1 票</button>
<button onClick={ () => setCountB(countB+1)}>B に 1 票</button>
</div>
);
}
export default App;
countA と countB の状態を 0 で作って
押すと加算される ボタンをそれぞれ作る。
Title, SubTitle に console.log を追加する
type Props = { titleText: string }
const Title: React.FC<Props> = ({titleText}) => {
console.log('Title rendered');
return (
<h2> {titleText} </h2>
);
}
export default Title;
Title と SubTitle の内部で console.log を呼ぶようにしておく
countA, countB が動いた時に Title, SubTitle まで再度読み込まれているところを確認る
そうすると、countA, countB が変更して再レンダリングされたタイミングで
全く関係ない Title, SubTitle コンポーネントまで再度読み込まれてしまっているのがわかる。
これを useMemo を使って改善していく。
Title コンポーネントで React.memo を引数に追加して中身を () で囲う。
const Title: React.FC<Props> = React.memo(({titleText}) => {
console.log('Title rendered');
return (
<h2> {titleText} </h2>
);
})
すると count の変化で Title コンポーネントが変化しなくなる。
一方、Button を共通コンポーネント化すると
countA が動いた時でも ButtonA だけでなく
ButtonB まで動いてしまう問題が残っている。
これを
Counter コンポーネントを作成して使って countA, countB を表示するようにする
import React from 'react'
type Props = {
counterTitle: string;
count: number;
}
const Counter: React.FC<Props> = React.memo(({counterTitle, count}) => {
console.log(`Counter: ${counterTitle} rendered`);
return (
<div> {counterTitle}: <span>{count}人</span></div>
);
})
export default Counter;
counterTitle と count を受け取って表示するコンポーネントを作って
<Counter counterTitle={'A'} count={countA} />
<Counter counterTitle={'B'} count={countB} />
App で呼び出す
Button コンポーネントを共通化して onClick と buttonText を受け取って {A,B} に一票を動かせるようにする
import React from 'react'
type Props = {
buttonText: string;
onClick: () => void;
};
const Button: React.FC<Props> = React.memo(({ buttonText, onClick }) => {
console.log(`Button:${buttonText} rendered`);
return (
<div >
<button onClick={onClick} type='button' >
{buttonText}
</button>
</div>
);
});
export default
buttonText と onClick を受けとる
Button コンポーネントを作って
<Button onClick={handleCountUpA} buttonText='A に 1 票' />
<Button onClick={handleCountUpB} buttonText='B に 1 票' />
A に一票追加
B に一票追加
これらを App でこのコンポーネントで動かす。
handleCountUp{A,B} に useCallback を count{A,B} を引数で組み込んで動作時に App 全体を読み込まれないようにする
const handleCountUpA = () => {
setCountA(countA + 1)
}
この handleCountUpA を
const handleCountUpA = useCallback(() => {
setCountA(countA + 1)
}, [countA])
useMemo と同じように 引数から関数が閉じるまでの
() => {}
の部分を useCallback() でくくる。
useEffect と同じように対象の変数を指定する(必須)
useCallBack
useCallBack なしで B に一票ボタンをクリックすると
A ボタンまで再度読み込まれてしまう。
ここにさっきの useCallback を追加して
B ボタンを押すと
今度は B ボタンのみが再描画された。
まとめ
通常は App の useState で実装された状態変数が変更されると
中の全てのコンポーネントが再描画されてしまう。
これは計算の無駄であり、パフォーマンスの低下につながる。
const Title: React.FC<Props> = React.memo(({titleText}) => {
console.log('Title rendered');
return (
<h2> {titleText} </h2>
);
})
全く別のコンポーネントであれば
このように React.memo() で引数から関数の終わりまで括ると
引数に変化がないときは、関係ない状態変数が変化しても
再描画されないようになる。
Button のように複数のコンポーネントで関数を渡して使われている汎用コンポーネントでは、React.memo() を使っても、どれかの Button が使われるたびに全ての Button が再描画されてしまう。
そこで Button の onClick に仕込むハンドル関数自体に
const handleCountUpA = useCallback(() => {
setCountA(countA + 1)
}, [countA])
このように useCallback() で括って引数に特定の状態変数を取れば
その状態引数を使ったコンポーネントのみ描画されるようになる。
countA, countB の状態があってそれぞれ
ButtonA, ButtonB があると
countA の変化で ButtonA のみが再描画されるようになる。
Top comments (0)