【React】再レンダリングの仕組みと最適化
React初心者です。
Reactのレンダリングについて学習したのでまとめてみました。
Reactが再レンダリングするタイミング
基本的にReactで再レンダリングが起きるタイミングは以下の3つ。
- stateが更新された時
- propsが更新された時
- 親コンポーネントが再レンダリングされた時
1. stateが更新された時
import { useState } from "react";
export default function App() {
console.log("App");
const [text, setText] = useState("");
const changeText = (e) => {
setText(e.target.value);
};
return (
<>
<p>App component</p>
<input type="text" onChange={changeText} />
</>
);
}
上記のコードで空欄に文字を入力・削除してみると、処理回数に応じてconsoleに App
が繰り返される。
すなわち、stateが変更されることで再レンダリングが発生している。
2. propsが更新された時
import { useState } from "react";
import Child from "./components/Child";
export default function App() {
const [count, setCount] = useState(0);
const countUp = () => {
setCount(count + 1);
};
return (
<>
<p>App component</p>
<button onClick={countUp}>count up</button>
<Child count={count} />
</>
);
}
const Child = (props) => {
const { count } = props;
console.log("Child");
return (
<>
<p>Child component</p>
{count}
</>
);
};
export default Child;
親であるApp
コンポーネントから子であるChild
コンポーネントへcount
というpropsを渡している。
ブラウザで実行してcount up
ボタンを押すと数字が1ずつ増えていき、consoleにはChild
が回数分表示される。
すなわち、propsが変更されることで再レンダリングが発生している。
3. 親コンポーネントが再レンダリングされた時
これは「親コンポーネントで再レンダリングが発生すると、その配下にある子コンポーネントが全て再レンダリングされてしまう」というもの。
import { useState } from "react";
import Child from "./components/Child";
export default function App() {
console.log("App");
const [text, setText] = useState("");
const changeText = (e) => {
setText(e.target.value);
};
return (
<>
<p>App component</p>
<input type="text" onChange={changeText} />
<br />
<Child />
</>
);
}
const Child = () => {
console.log("Child");
return (
<>
<p>Child component</p>
</>
);
};
export default Child;
上記のコードでは親子間でのpropsの受け渡しはないが、ブラウザ上で空欄に文字を入力・削除すると(親であるApp
コンポーネントに記述されているinput
要素に値を入力すると)、consoleにApp
とChild
が繰り返し表示されることが確認できる。
すなわち、親コンポーネントが再レンダリングされているタイミングで子コンポーネントも再レンダリングされている。
親子間で値の受け渡しが無いのにも関わらず、意図せずこのような再レンダリングが発生してしまうことで、パフォーマンスが下がってしまう。
再レンダリングを最適化する
再レンダリングを最適化する、すなわち無駄な計算や処理を抑えるために必要なReactの機能が以下の3つ。
- memo
- useCallback
- useMemo
これらの機能を用いることで、メモ化(計算結果を保持し、それを再利用すること)ができる。
同じ結果を返す処理に関しては初回のみ処理を実行しておき、2回目以降は前回の処理結果を呼び出すことで毎回同じ処理を実行しなくてよくなる。
1. memo
以下のコードをブラウザで実行し、空欄に文字を入力・削除すると(親であるApp
コンポーネントに記述されているinput
要素に値を入力すると)、consoleにはApp
のみが繰り返し表示される。(メモ化していない先ほどのコードではApp
とChild
が交互に繰り返されていた)
Childコンポーネントはメモ化されているので、Child
はconsoleに表示されない。
import { useState } from "react";
import Child from "./components/Child";
export default function App() {
console.log("App");
const [text, setText] = useState("");
const changeText = (e) => {
setText(e.target.value);
};
return (
<>
<p>App component</p>
<input type="text" onChange={changeText} />
<br />
<Child />
</>
);
}
import { memo } from "react";
const Child = memo(() => {
console.log("Child");
return (
<>
<p>Child component</p>
</>
);
});
export default Child;
2. useCallback
useCallback
はメモ化したコールバック関数を返すHooks API。
次に、以下のようなコードを実行してみる。
import { useState } from "react";
import Child from "./components/Child";
export default function App() {
console.log("App");
const [text, setText] = useState("");
const changeText = (e) => {
setText(e.target.value);
};
return (
<>
<p>App component</p>
<br />
<Child changeText={changeText} />
</>
);
}
import { memo } from "react";
const Child = memo((props) => {
console.log("Child");
const { changeText } = props;
return (
<>
<input type="text" onChange={changeText} />
<p>Child component</p>
</>
);
});
export default Child;
先ほど親コンポーネント(App)で処理していたinput要素を子コンポーネント(Child)に移動させ、関数changeText
もpropsで受け渡している。
以下のコードをブラウザで実行し、空欄に文字を入力・削除すると(親であるApp
コンポーネントに記述されているinput
要素に値を入力すると)、再びconsoleにApp
とChild
が繰り返し表示される。
「Child
コンポーネントをメモ化しているのになぜ?」
この原因はpropsで受け渡した関数にある。
親コンポーネントで生成した関数をpropsで子コンポーネントに渡すと、関数の内容が同じでも子コンポーネントでは「毎回新しい関数が渡されている」と判断されてしまう。
そこでuseCallback
を使う。
import { useState, useCallback } from "react";
import Child from "./components/Child";
export default function App() {
console.log("App");
const [text, setText] = useState("");
const changeText = useCallback(
(e) => {
setText(e.target.value);
},
[setText]
);
return (
<>
<p>App component</p>
<br />
<Child changeText={changeText} />
</>
);
}
以上のように関数(changeText)をuseCallback
で囲み、第2引数には配列を設定できる。(useEffect
と同様)
このコードをブラウザで実行し、空欄に文字を入力・削除すると(親であるApp
コンポーネントに記述されているinput
要素に値を入力すると)、consoleにはApp
のみが繰り返し表示される。
3. useMemo
useMemo
は変数のメモ化ができるHooks API。
import { useState } from "react";
export default function App() {
console.log("App");
const [text, setText] = useState("");
const changeText = (e) => {
setText(e.target.value);
};
const todayDate = () => {
console.log("Date");
const dateObj = new Date();
const dateString = `${dateObj.getFullYear()}年${
dateObj.getMonth() + 1
}月${dateObj.getDate()}日`; // YYYY年MM月DD日
return <p>日付:{dateString}</p>;
};
return (
<>
<p>App component</p>
<input type="text" onChange={changeText} />
<br />
{todayDate()}
</>
);
}
以上のコードをブラウザで実行し、空欄に文字を入力・削除すると(親であるApp
コンポーネントに記述されているinput
要素に値を入力すると)、consoleにApp
とDate
が交互に繰り返し表示される。
import { useState, useMemo } from "react";
export default function App() {
console.log("App");
const [text, setText] = useState("");
const changeText = (e) => {
setText(e.target.value);
};
const todayDate = useMemo(() => {
console.log("Date");
const dateObj = new Date();
const dateString = `${dateObj.getFullYear()}年${
dateObj.getMonth() + 1
}月${dateObj.getDate()}日`; // YYYY年MM月DD日
return <p>日付:{dateString}</p>;
}, []);
return (
<>
<p>App component</p>
<input type="text" onChange={changeText} />
<br />
{todayDate}
</>
);
}
以上のように変数を返す関数(todayDate)をuseMemo
で囲み、第2引数には配列を設定できる。(useEffect
と同様)
このコードをブラウザで実行し、空欄に文字を入力・削除すると(親であるApp
コンポーネントに記述されているinput
要素に値を入力すると)、consoleにはApp
のみが繰り返し表示される。
Discussion
2. propsが更新された時
のサンプルコードでChildが再レンダリングされているのはpropsが更新されたからではなく、AppでsetCountを実行してAppが再レンダリングされているからです。つまり、3. 親コンポーネントが再レンダリングされた時
と同じ理由で再レンダリングされています。useStateでカウントせずにuseRefでカウントしたものをpropsとして渡すようにすると、propsが更新されているにもかかわらずChildが再レンダリングされないことを確認できます。すなわち2. propsが更新された時
は誤りだとわかります。確かにご指摘の通り2の
propsが更新された時
の例は3. 親コンポーネントが再レンダリングされた時
と同じになってしまってるのですが、オフィシャルにある通り基本的にpropsが更新されるとデフォルトでは再レンダリングされるので2. propsが更新された時は誤り
ではないという理解です。違ったらご指摘ください。親の立場で「あるデータをpropsとして子に渡す」と言えますが、それは「親のpropsではない」ですよね?用語集を見ても「propsはリードオンリーだ」とあるように子の立場としてみた場合にpropsと言っています。
引用されている説明文は
forceUpdate()
のものですが、your component
とは子の立場になると思います。親がpropsとして渡しているstateを更新すると親の再レンダリングが発生し、それに伴って子が再レンダリングされますが、その際には子としてはpropsが変更されていると言えると思います。一方で、本記事では「親から子に渡すpropsが変更された時に再レンダリングされる」という趣旨で
2. propsが更新された時
と書かれているのでそれは誤りだという指摘です。