React 再レンダリング (useCallback useMemo)
はじめに
最近 React の再レンダリングについて理解が深まったので少しまとめようと思いました。
フロントエンドとバックエンドを兼業しているとどちらの知識も薄くなりがちで意外と基本的な部分を抑えるのに時間がかかる、、、
再レンダリングの仕組み
ここの記事でレンダリングの仕組みを詳しく説明しているので参考にしてください。ここでは再レンダリングの仕組みを解説します。
主には再レンダリングのタイミングは以下のとおりです。
- stateに変化が起きた時
- 親コンポーネントが再レンダリングされた時
stateに変化が起きた時
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((count) => count + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+</button>
</div>
);
};
export default App;
こちらが実際の動きになります。
button
をクリックすると count が増えて、state が更新される処理です。React dev tool
で確認すると再レンダリングされていることが確認できます。
親コンポーネントが再レンダリングされた時
import { useState } from "react";
const Child = () => {
return <div>child</div>;
};
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((count) => count + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+</button>
<Child />
</div>
);
};
export default App;
こちらが実際の動きになります。
上と同様にbutton
をクリックすると count が増えて、state が更新される処理です。React dev tool
で再レンダリングされていることが確認でき、子コンポーネントのChild
も再レンダリングされています。
再レンダリングを防ぐ方法
上記の子コンポーネントに関しては特に変更がないのに再レンダリングされています。この再レンダリングは不要であり、このような無駄な再レンダリングがパフォーマンスを下げる原因になったりします。ここからいくつかの再レンダリングを防ぐ例を上げていきます。
React.memo を使う方法
親コンポーネントが再レンダリングされた項目でも記載しましたが、無駄な再レンダリングはパフォーマン図を下げる原因になります。そこで React.memo
というものを紹介します。これを使うと親コンポーネントが再レンダリングされたときに子コンポーネントの再レンダリングを防ぐことができます。実際にコードを見てみましょう。
import React, { useState } from "react";
const Child = React.memo(() => {
return <div>child</div>;
});
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((count) => count + 1);
};
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+</button>
<Child />
</div>
);
};
export default App;
こちらが実際の動きになります。
このように子コンポーネントを React.memo
を使って定義し直すとChild
は再レンダリングされなくなりました。
useCallback を使う方法
関数を子コンポーネントにわたすような場合にも注意する必要があります。
const Child: React.FC<ChildProps> = ({ onClick }) => {
console.log("Child is rendering");
return <button onClick={onClick}>Increase Count</button>;
};
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("Count increased");
setCount((count) => count + 1);
};
return (
<div>
<p>Count: {count}</p>
<Child onClick={handleClick} />
</div>
);
};
export default App;
上記のようにhandleClick
をそのまま渡してしまうと意図しない無駄なレンダリングがされてしまいます。この無駄なレンダリングを防ぐために useCallback
を使い以下のように修正しました。
import React, { useState, useCallback } from "react";
type ChildProps = {
onClick: () => void;
};
const Child: React.FC<ChildProps> = ({ onClick }) => {
console.log("Child is rendering");
return <button onClick={onClick}>Increase Count</button>;
};
const App = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Count increased");
setCount((count) => count + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<Child onClick={handleClick} />
</div>
);
};
export default App;
こちらが実際の挙動です。
これで再レンダリングの挙動を確認すると、なぜか子コンポーネントも再レンダリングされている、、、
これは、handleClick
をメモ化して再レンダリングを防ぐためのものですが、Child
コンポーネント自体は再レンダリングを防げていないのでコンポーネント自体もメモ化する必要があります。
import React, { useState, useCallback } from "react";
type ChildProps = {
onClick: () => void;
};
const Child: React.FC<ChildProps> = React.memo(({ onClick }) => {
console.log("Child is rendering");
return <button onClick={onClick}>Increase Count</button>;
});
const App = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Count increased");
setCount((count) => count + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<Child onClick={handleClick} />
</div>
);
};
export default App;
こちらが実際の挙動になります。
上記のこうにコードを修正するとメモ化が防ぐことができました。
アロー関数でも再レンダリングので注意!!(補足)
実際に業務で自分がやっていて指摘されたことを上げます。以下のように onClick={() => handleClick()}
とやってしまうとせっかくメモ化した関数を再生成され、無駄なレンダリングをしてしまうので注意してください。
import React, { useState, useCallback } from "react";
type ChildProps = {
onClick: () => void;
};
const Child: React.FC<ChildProps> = React.memo(({ onClick }) => {
console.log("Child is rendering");
return <button onClick={onClick}>Increase Count</button>;
});
const App = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Count increased");
setCount((count) => count + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<Child onClick={() => handleClick()} />
</div>
);
};
export default App;
Discussion