目次
コンポーネントが再レンダリングされる条件について
コンポーネントは以下の条件のいずれかに当てはまると再レンダリングされます。
- コンポーネントのStateが更新された時
- 親コンポーネントがレンダリングされた時
以下では再レンダリングが発生する具体例について紹介します。
コンポーネントのStateが更新された時
以下のExample.tsx
はStateでcount
という数値を管理しています。count
の更新に応じてExample.tsx
はレンダリングされます。
Example.tsx
import { useEffect, useState } from "react";
import "./Example.css";
const Example: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('useEffect is called')
// レンダリングの変化をわかりやすくするように非同期にしている
const increment = async () => {
await new Promise((s) => setTimeout(s, 2000));
setCount((prevCount) => prevCount + 1);
};
increment();
}, []);
console.log(`Example.tsx: count is ${count}`);
console.log("Example.tsx: render");
return <div className="Example">{count}</div>;
};
export default Example;
実行ログ
Example.tsx: count is 0
Example.tsx: render
useEffect is called
Example.tsx: count is 1
Example.tsx: render
上記で紹介したExample.tsx
の時間経過による変化は以下の通りです。
- 初回レンダリングされる
- マウントされる
- 画面が表示される(このときcountは0)
- マウント後、useEffectが実行される
- useEffectによってStateが更新される
- Stateが更新されたので再度レンダリングされる
- 画面が更新される(このときcountは1)
参考: 複数のStateを持つ場合、各Stateが更新されるたびにレンダリングが実行される
コンポーネントが複数のStateを管理する場合は各Stateが変化するたびにレンダリングが実行されます。
以下のExample.tsx
はStateでcount
という数値とisDisplay
という真偽値を管理しています。count
あるいはisDisplay
が更新されるたびにExample.tsx
はレンダリングされます。
Example.tsx
import { useEffect, useState } from "react";
import "./Example.css";
const Example: React.FC = () => {
const [count, setCount] = useState(0);
const [isDisplay, setIsDisplay] = useState(false);
useEffect(() => {
const increment = async () => {
await new Promise((s) => setTimeout(s, 2000));
setCount((prevCount) => prevCount + 1);
};
increment();
}, []);
useEffect(() => {
const displayable = async () => {
await new Promise((s) => setTimeout(s, 4000));
setIsDisplay(true);
};
displayable();
}, []);
console.log(`Example.tsx: count is ${count}`);
console.log(`Example.tsx: isDisplay is ${isDisplay}`);
console.log("Example.tsx: render");
return (
<div className="Example">
{count}
{isDisplay && <div>Now you can see the description!</div>}
</div>
);
};
export default Example;
実行ログ
Example.tsx: count is 0
Example.tsx: isDisplay is false
Example.tsx: render
Example.tsx: count is 1
Example.tsx: isDisplay is false
Example.tsx: render
Example.tsx: count is 1
Example.tsx: isDisplay is true
Example.tsx: render
親コンポーネントがレンダリングされた時
Propsの受け取り有無に関わらず、親コンポーネントのレンダリングに応じて子コンポーネントもレンダリングされます。
具体例は以下の通りです。
Example.tsx
import { useEffect, useState } from "react";
import "./Example.css";
import "./ExampleChild";
import ExampleChild from "./ExampleChild";
const Example: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const increment = async () => {
await new Promise((s) => setTimeout(s, 2000));
setCount((prevCount) => prevCount + 1);
};
increment();
}, []);
console.log(`Example.tsx: count is ${count}`);
console.log("Example.tsx: render");
return (
<div className="Example">
{count}
<ExampleChild />
</div>
);
};
export default Example;
ExampleChild.tsx
import "./ExampleChild.css";
const ExampleChild: React.FC = () => {
console.log("ExampleChild.tsx: render");
return <div className="ExampleChild">This is ExampleChild.tsx</div>;
};
export default ExampleChild;
実行ログ
Example.tsx: count is 0
Example.tsx: render
ExampleChild.tsx: render
Example.tsx: count is 1
Example.tsx: render
ExampleChild.tsx: render
親コンポーネントのレンダリングによるコンポーネントの再レンダリングを防ぐ方法
親コンポーネントの変更が子コンポーネントに影響を与えない場合、親コンポーネントのレンダリングに応じて子コンポーネントを再レンダリングさせる必要がありません。
親コンポーネントのレンダリングによる再レンダリングを制御する方法には以下の2つがあります。
- コンポーネントをメモ化する
- コンポジション(props.children)を利用する
以下ではそれぞれについて紹介します。
コンポーネントをメモ化する
コンポーネントのメモ化をするにはReact.memoもしくはuseMemoを利用します。
React.memoとuseMemoの詳細解説は具体例で理解するuseMemoとuseCallbackの使い方。Reactパフォーマンスチューニングで紹介しています。
React.memoを利用する方法
React.memoはクラスコンポーネントにおけるshouldComponentUpdate()やReact.PureComponentに相当する、コンポーネントをメモ化する機能です。
React.memoは高階コンポーネント(Higher-Order Component, HOC)の一種ですので、React.memo(コンポーネント)
という形でコンポーネントを囲むことで利用できます。
React.memoを利用したコンポーネントのメモ化は以下の通りです。
ExampleChild.tsx
+ import { memo } from "react";
import "./ExampleChild.css";
const ExampleChild: React.FC = () => {
console.log("ExampleChild.tsx: render");
return <div className="ExampleChild">This is ExampleChild.tsx</div>;
};
- export default ExampleChild;
+ export default memo(ExampleChild);
実行ログ
Example.tsx: count is 0
Example.tsx: render
ExampleChild.tsx: render
Example.tsx: count is 1
Example.tsx: render
useMemoを利用する方法
useMemoは関数の結果をメモ化するフックです。
useMemoはuseMemo(() => 結果を算出するロジック, 依存配列)
という形で利用します。
依存配列の要素のうちいずれかが変化した場合のみ再計算されます。
依存配列が設定されていない場合はレンダリングのたびに計算が実行されます。
useMemoはReact.memoのようにコンポーネントのメモ化もできます。
useMemoを利用したコンポーネントのメモ化は以下の通りです。
Example.tsx
+ import { useEffect, useMemo, useState } from "react";
- import { useEffect, useState } from "react";
import "./Example.css";
import "./ExampleChild";
import ExampleChild from "./ExampleChild";
const Example: React.FC = () => {
const [count, setCount] = useState(0);
+ const ExampleChildMemo = useMemo(() => <ExampleChild />, []);
useEffect(() => {
const increment = async () => {
await new Promise((s) => setTimeout(s, 2000));
setCount((prevCount) => prevCount + 1);
};
increment();
}, []);
console.log(`Example.tsx: count is ${count}`);
console.log("Example.tsx: render");
return (
<div className="Example">
{count}
+ {ExampleChildMemo}
- <ExampleChild />
</div>
);
};
export default Example;
実行ログ
Example.tsx: count is 0
Example.tsx: render
ExampleChild.tsx: render
Example.tsx: count is 1
Example.tsx: render
参考: コールバック関数をPropsで受け取る場合はuseCallbackを併用する
関数コンポーネント内で定義したアロー関数はレンダリングのたびに再作成されます。
ですので、コールバック関数をPropsで受け取る場合はコンポーネント自身をメモ化しただけではメモ化がうまく機能しません。
なぜなら、親コンポーネントがレンダリングされるたびにコールバック関数が新たに作成されるため、メモ化した子コンポーネントのProps(依存配列)が変化したとみなされるからです。
その結果、親コンポーネントがレンダリングされるたびに子コンポーネントもレンダリングされます。
コールバック関数をPropsで受けとるコンポーネントをメモ化する場合は、コンポーネントだけでなくコールバック関数もメモ化する必要があります。
コールバック関数のメモ化はuseCallbackを利用します。
具体例は以下の通りです。
+ import { useCallback, useEffect, useMemo, useState } from "react";
- import { useEffect, useState } from "react";
import "./Example.css";
import ExampleChild from "./ExampleChild";
const Example: React.FC = () => {
const [count, setCount] = useState(0);
const [isDisplay, setIsDisplay] = useState(false);
// コールバック関数のメモ化
+ const handleClick = useCallback(() => {
+ setCount((prevCount) => prevCount + 1);
+ }, []);
- const handleClick = () => {
- setCount((prevCount) => prevCount + 1);
- };
// コールバック関数をメモ化しているため、
// 親コンポーネントがレンダリングされてもコールバック関数は再作成されない
// その結果、コンポーネントのメモ化が正常に機能する
+ const ExampleChildMemo = useMemo(
+ () => <ExampleChild onClick={handleClick} />,
+ [handleClick]
+ );
useEffect(() => {
const displayable = async () => {
await new Promise((s) => setTimeout(s, 2000));
setIsDisplay(true);
};
displayable();
}, []);
console.log(`Example.tsx: count is ${count}`);
console.log(`Example.tsx: isDisplay is ${isDisplay}`);
console.log("Example.tsx: render");
return (
<div className="Example">
{count}
{isDisplay && <div>Now you can see the description!</div>}
+ {ExampleChildMemo}
- <ExampleChild onClick={handleClick} />
</div>
);
};
export default Example;
ExampleChild.tsx
import "./ExampleChild.css";
type ChildProps = {
onClick: () => void;
};
const ExampleChild: React.FC<ChildProps> = ({ onClick }) => {
console.log("ExampleChild.tsx: render");
return (
<div className="ExampleChild">
<button onClick={onClick}>count+</button>
</div>
);
};
export default ExampleChild;
実行ログ
Example.tsx: count is 0
Example.tsx: isDisplay is false
Example.tsx: render
ExampleChild.tsx: render
Example.tsx: count is 0
Example.tsx: isDisplay is true
Example.tsx: render
#
# useCallbackを利用しないと isDisableがtrueに変化したこのタイミングでExampleChildが再レンダリングされる
#
useCallbackの詳細解説は具体例で理解するuseMemoとuseCallbackの使い方。Reactパフォーマンスチューニングで紹介しています。
コンポジション(props.children)を利用する
親コンポーネントにコンポジションモデルを採用し、親コンポーネントから子コンポーネントをprops.children
で呼び出します。
コンポジションを利用することで親コンポーネントのレンダリングによる再レンダリングを防げます。
具体例は以下の通りです。
index.tsx
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import Example from "./Example";
+ import ExampleChild from "./ExampleChild";
ReactDOM.render(
<React.StrictMode>
+ <Example>
+ <ExampleChild />
+ </Example>
- <Example />
</React.StrictMode>,
document.getElementById("root")
);
Example.tsx
import { useEffect, useState } from "react";
import "./Example.css";
- import "./ExampleChild";
- import ExampleChild from "./ExampleChild";
+ const Example: React.FC = ({ children }) => {
- const Example: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const increment = async () => {
await new Promise((s) => setTimeout(s, 2000));
setCount((prevCount) => prevCount + 1);
};
increment();
}, []);
console.log(`Example.tsx: count is ${count}`);
console.log("Example.tsx: render");
return (
<div className="Example">
{count}
+ {children}
- <ExampleChild />
</div>
);
};
export default Example;
実行ログ
Example.tsx: count is 0
Example.tsx: render
ExampleChild.tsx: render
Example.tsx: count is 1
Example.tsx: render
今回のまとめ
- コンポーネントが再レンダリングされるトリガは『Stateの更新』もしくは『親コンポーネントの更新』
- 親コンポーネントの更新による再レンダリングを防ぐには『コンポーネントのメモ化』もしくは『コンポジションモデルの採用』をする
- コンポーネントをメモ化するにはReact.memoもしくはuseMemoを利用する
- コールバック関数をPropsで受け取るコンポーネントをメモ化する際はuseCallbackで関数もメモ化する必要がある
さいごに
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!