目的
ReactでContextを利用している子コンポーネントと再レンダリングのトリガの関連が不明だったので自分で動かして調べる。
不明点
Reactにはコンポーネントの不要な再レンダリングを防ぐ手段がある。
- shouldComponentでfalseを返す
- React.memo使う
- PureComponent使う
一方でContextはpropsのバトンリレーをスキップすることが可能。
疑問なのはContext Provider(親) -> 中間コンポーネント -> Context Consumer(子)のような構造になっているときに中間コンポーネントがshouldComponentとかを使って再レンダリングされなかったときでもContextが変更されたときにはConsumerが再レンダリングされるかどうか。
Contextの目的からすれば再レンダリングされないとおかしいが、公式ドキュメントでははっきりしないので動かして試してみた。
結論
Contextをconsumeしている子コンポーネントは途中のコンポーネントが再レンダリングをスキップしていても再レンダリングされる。
検証Code
import React, { useContext } from "react";
import "./App.css";
const ThemeContext = React.createContext({
mode: "light",
changeMode: () => {}
});
function Toolbar() {
return (
<>
<ContextConsumingClass />
<ContextConsumingFuncMemo />
</>
);
}
const ToolbarMemo = React.memo(Toolbar, (prevProps, nextProps) => {
return true; // Always avoid re-render
});
class ContextConsumingClass extends React.Component {
static contextType = ThemeContext;
shouldComponentUpdate() {
return false;
}
render() {
console.log("ContextConsumingClass:render()");
return <p>ContextConsumingClass: I'm actually not using context :)</p>;
}
}
function ContextConsumingFunc() {
const context = useContext(ThemeContext);
console.log("ContextConsumingFunc:render()");
return <p>ContextConsumingFunc: I'm actually not using context :)</p>;
}
const ContextConsumingFuncMemo = React.memo(ContextConsumingFunc, (prevProps, nextProps) => {
return true; // Always avoid re-render
});
class App extends React.Component {
state = {
mode: "dark"
};
changeMode = () => {
this.setState(prevState => {
if (this.state.mode === "dark") {
return { mode: "light" };
} else {
return { mode: "dark" };
}
});
};
render() {
return (
<ThemeContext.Provider
value={{ mode: this.state.mode, changeMode: this.changeMode }}
>
<div className="App">
<ToolbarMemo></ToolbarMemo>
<button onClick={this.changeMode}>change mode</button>
<p>{this.state.mode}</p>
</div>
</ThemeContext.Provider>
);
}
}
export default App;
ブラウザでReact DevToolsを使ってフレームキャプチャしつつ、ボタンを押してContextにかかわるstateを変更してみたときの状態。中間のToolbar(Memo)と ContextConsumingFuncMemoが再レンダリングされていない(灰色)にも関わらずContextConsumingClassとContextConsumingFuncコンポーネントはちゃんと再レンダリングされている。ContextConsumingClassについては自身のshouldComponentUpdate()で毎回falseを返していてもContextに変更があるとrender()が呼び出されている。