目的
Reactを学習しています。備忘録としてレンダリングについて自分なりにまとめてみました。誤りがあれば指摘をお願いします。
レンダリングとは?
Reactがコンポーネントを呼び出すことをレンダリングという。
レンダリングされるきっかけ
大きく分けると下記の2つがある。
①コンポーネントの初期レンダリング(画面の初期表示)
→rootのコンポーネントが呼び出される時
②stateが更新された時の再レンダリング
→状態(state)が変更された時
再レンダリングのタイミング
①propsの変更
②stateの変更
③親コンポーネントでレンダリングがあった場合
サンプルコード(実際に動かしてみる)
インプットエリアとボタンでコンポーネントの表示/非表示を切り替えができる。
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")//初期表示のレンダリング
);
import { useState } from "react";
import './App.css';
import { ChildArea } from './ChildArea';
export default function App() {
console.log("App");
const [text, setText] = useState();
const [open, setOpen] = useState(false);
const onChangeText = (e) => setText(e.target.value);
const onClickOpen = () => setOpen(!open);
return (
<div className="App">
<input value={text} onChange={onChangeText}/>
<br />
<br />
<button onClick={onClickOpen}>表示</button>
<ChildArea open={open}/>
</div>
);
}
const style = {
width:"150px",
height: "200px",
backgroundColor: "khaki"
}
export const ChildArea = (props) => {
const { open } = props;
console.log("ChildAreaがレンダリングされた!!");
const data = [...Array(2000).keys()];
data.forEach(()=>{
console.log("...");
});
return (
<>
{open ? (
<div style={style}>
<p>子コンポーネント</p>
</div>
) : null}
</>
);
};
再レンダリングが起きる例①propsの変更
表示ボタンを押して、子コンポーネントの表示を切り替える。
表示ボタン押下後、子コンポーネントが表示される!
コンソールで確認
1回目の「ChilrenAreがレンダリングされた!!」は、初期表示
2回目の「ChilrenAreがレンダリングされた!!」は、表示ボタンを押したことによる再レンダリング
流れは、
表示ボタンを押す→onClickOpenが呼ばれる→setOpenが呼ばれる→openの状態が変わる(false→true)。
子コンポーネント(ChildArea)にopenプロパティをpropsとして渡していて、表示ボタンを押すと、propsの値が変化、再レンダリングされる。
再レンダリングが起きる例②stateの変更③親コンポーネントでレンダリングがあった場合
インプットエリアに文字を入力する。
流れ
インプットエリアに文字入力→onChanegeTextが呼ばれる→setTextが呼ばれる→textの状態が変わる→Appレンダリング→子コンポーネントのChilrenAreもレンダリングされる
不要な再レンダリングがなぜよくないのか
このような感じで再レンダリングが起こります。再レンダリング自体は状態の変化が起こった際に生じるので問題はないように思います。しかし、再レンダリングで変更されたコンポーネントだけでなく、その子コンポーネントも再レンダリングが起きると、パフォーマンスに影響を与える可能性があります。
再レンダリングを最適化する方法
①memo化(コンポーネントに対して)
②useCallback(関数に対して)
③useMemo(値に対して)
①memo(コンポーネントに対して)
コンポーネントをmemoで囲うことによってpropsが変更されない限り、再レンダリングを起こさないようにする。
つまり親コンポーネントの再レンダリングに伴うコンポーネントの再レンダリングを防ぐ
export const ChildArea = memo((props) => {
const { open } = props;
console.log("ChildAreaがレンダリングされた!!");//これがコンソールに表示されたらChilrenAreがレンダリングされたということ
const data = [...Array(2000).keys()];
data.forEach(()=>{
console.log("...");
});
return (
<>
{open ? (
<div style={style}>
<p>子コンポーネント</p>
</div>
) : null}
</>
);
});
②useCallback(関数に対して)
先ほどのサンプルコードに子コンポーネントを閉じるonClickClose関数を追加すると、インプットエリアに文字を入力しても、memoが効かなくなり、再レンダリングが起こってしまう。
export default function App() {
console.log("App");
const [text, setText] = useState();
const [open, setOpen] = useState(false);
const onChangeText = (e) => setText(e.target.value);
const onClickOpen = () => setOpen(!open);
const onClickClose = () => setOpen(false);//追加
return (
<div className="App">
<input value={text} onChange={onChangeText}/>
<br />
<br />
<button onClick={onClickOpen}>表示</button>
<ChildArea open={open} onClickClose={onClickClose}/>//追加
</div>
);
}
export const ChildArea = memo((props) => {
const { open, onClickClose } = props;//追加
console.log("ChildAreaがレンダリングされた!!");//これがコンソールに表示されたらChilrenAreがレンダリングされたということ
const data = [...Array(2000).keys()];
data.forEach(()=>{
console.log("...");
});
return (
<>
{open ? (
<div style={style}>
<p>子コンポーネント</p>
<button onClick={onClickClose}>閉じる</button>//追加
</div>
) : null}
</>
);
});
原因はアロー関数onClickCloseをpropsとして、ChilrenAreに渡しているためである。
流れとしては、
インプットエリアに文字を入力する→onChangeText関数が呼ばれる→App関数がレンダリングされる→onClickClose関数が生成される→生成された新しいonClickCloseプロパティがpropsとして渡される→ChilrenAreで再レンダリングが起こる!!!
というような感じ。onClickCloseに変更なくても、Appが再レンダリングされる度に新しく生成されるため、変更があった(新しく異なるものが生成された)としてonClickCloseプロパティがpropsとしてChilrenAreに渡されてしまう。
なので、setOpen関数をuseCallbackで囲う
第二引数に設定した値が変更されたかどうかで処理が動くようにする。下記でいうと、Appレンダリングに伴ってonClickCloseが生成されてもsetOpenに変更がないため、同じものを使い回すようにする。
import { useState, useCallback } from "react";
const onClickClose = useCallback(() => setOpen(false), [setOpen]);
//useEffectと同様に第二引数に設定した値が変更されたら処理が動くようにする、空配列でもよい
③useMemo(変数に対して)
import { useState, useCallback, useMemo } from "react";
const temp = useMemo(() => 1 + 3, []);
console.log(temp);
この場合だと1 + 3に対して、useMemoで囲っているため、1 + 3(処理では4)のまま変更されることなく使い回される。使用頻度は低いが、覚えておくと便利。
参考記事
Udemy Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版
React再レンダリングガイド: 一度に全て理解する