はじめに
ReactにおけるContextAPIではuseStateを無造作に使うとレンダリングコストが増大し、結果としてパフォーマンスが低下してしまう恐れがあります。
そこで、この記事ではuseMemoを用いて再レンダリングするコンポーネントを選択的に記述することでパフォーマンスの改善を行う方法について触れています。
コンポーネント構成
(※ここではわかりやすいよう、コンポーネントに適宜CSSを当てています)
対策なしバージョン
コードサンプル
import { createContext } from "react";
// Contextの型を定義
type ValueContextType = {
value1: number;
updateValue1: (_: number) => void;
value2: number;
updateValue2: (_: number) => void;
value3: number;
updateValue3: (_: number) => void;
};
// Contextを作成
const ValueContext = createContext<ValueContextType>(null!);
// Providerを作成
const ValueProvider = ({ children }: { children: JSX.Element }) => {
const [value1, setValue1] = useState(0);
const updateValue1 = (value: number) => {
setValue1(value);
};
const [value2, setValue2] = useState(0);
const updateValue2 = (value: number) => {
setValue2(value);
};
const [value3, setValue3] = useState(0);
const updateValue3 = (value: number) => {
setValue3(value);
};
return (
<ValueContext.Provider value={{
value1,
updateValue1;
value2,
updateValue2;
value3,
updateValue3;
}}>
{children}
</ValueContext.Provider>
);
};
// ContextとProviderをエクスポート
export { ValueContext, ValueProvider };
import { useContext } from "react";
import { ValueContext } from "./Context.tsx";
import Child1 from "./Child1";
import Child2 from "./Child2";
// Parentコンポーネント
const Parent = () => {
console.log("re-rendered Parent");
const { value1, updateValue1 } = useContext(ValueContext);
return (
<div>
<h2>Parent</h2>
<p>{value1}</p>
<button type="button" onClick={() => {
updateValue1(value1 + 1);
}}>+</button>
<Child1 />
<Child2 />
</div>
);
};
export default Parent;
import { useContext } from "react";
import { ValueContext } from "./Context.tsx";
// Child1コンポーネント
const Child1 = () => {
console.log("re-rendered Child1");
const { value2, updateValue2 } = useContext(ValueContext);
return (
<div>
<h2>Child1</h2>
<p>{value2}</p>
<button type="button" onClick={() => {
updateValue2(value2 + 1);
}}>+</button>
</div>
);
};
export default Child1;
import { useContext } from "react";
import { ValueContext } from "./Context.tsx";
// Child2コンポーネント
const Child2 = () => {
console.log("re-rendered Child2");
const { value3, updateValue3 } = useContext(ValueContext);
return (
<div>
<h2>Child2</h2>
<p>{value3}</p>
<button type="button" onClick={() => {
updateValue3(value3 + 1);
}}>+</button>
</div>
);
};
export default Child2;
export default function MyApp() {
return (
<ValueProvider>
<Parent />
</ValueProvider>
);
}
問題点
1. value1が更新された時、Child1, Child2コンポーネントも再レンダリングされる
これに関しては、親コンポーネントが再レンダリングされると子コンポーネントも再レンダリングされるというReactにおける原則によるものなので必ずしも問題であるとは言えないですが、アプリケーションが大規模になるにつれてパフォーマンスの低下を引き起こす可能性があります。
【参考記事】
2. value2, value3が更新された時、Parentコンポーネントも再レンダリングされる
value2, value3の値が更新された時、値が変わった子コンポーネントのみ再レンダリングされて欲しいところ、子コンポーネントの更新のみならず、親コンポーネント, 兄弟コンポーネントの再レンダリングという望ましくない挙動が起きています。どちらかと言うとこっちの方が問題になってくると思います。
この再レンダリングの原因は、Contextの値が変更されるとuseContextでContextを呼び出しているすべてのコンポーネントが再レンダリングされるというContextAPIの仕組みによるものです。
(@honey32さん、ご指摘ありがとうございました。)
【公式ドキュメントからの引用】
All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update.
(訳)
Providerの子孫であるすべてのConsumerは、Providerのvalue propが変更されるたびに再レンダリングされます。Providerからその子孫Consumerへの伝播は、shouldComponentUpdate
メソッドの対象ではないため、祖先コンポーネントが更新をスキップした場合でも、Consumerは更新されます。
ここでいうProviderのvalue propは
<ValueContext.Provider value={{
value1,
updateValue1;
value2,
updateValue2;
value3,
updateValue3;
}}>
この部分を指します。
すなわち、一度Contextのpropの値が更新されると
const { (略) } = useContext(ValueContext);
というコードが記述されているコンポーネントすべてが更新されてしまうということです。こちらもReactの設計思想を考えると当然の結果ではあるのですが、やはりこちらもパフォーマンスの低下を引き起こす可能性があります。
【参考記事】
改善策
コンポーネントが返すDOM(正確にはReact要素)を、Reactの標準APIであるuseMemoを用いてメモ化することで、不要なレンダリングを抑えることができます。
対策ありバージョン
コードサンプル
import { useContext, useMemo } from "react";
import { ValueContext } from "./Context.tsx";
import Child1 from "./Child1";
import Child2 from "./Child2";
// Parentコンポーネント
const Parent = () => {
const { value1, updateValue1 } = useContext(ValueContext);
return useMemo(() => { // React要素をメモ化
console.log("re-rendered Parent");
return (
<div>
<h2>Parent</h2>
<p>{value1}</p>
<button type="button" onClick={() => {
updateValue1(value1 + 1);
}}>+</button>
<Child1 />
<Child2 />
</div>
);
}, [value1]); // value1が更新されたときのみ再レンダリング
};
export default Parent;
import { useContext, useMemo } from "react";
import { ValueContext } from "./Context.tsx";
// Child1コンポーネント
const Child1 = () => {
const { value2, updateValue2 } = useContext(ValueContext);
return useMemo(() => { // React要素をメモ化
console.log("re-rendered Child1");
return (
<div>
<h2>Child1</h2>
<p>value2: {value2}</p>
<button type="button" onClick={() => {
updateValue2(value2 + 1);
}}>+</button>
</div>
);
}, [value2]); // value2が更新されたときのみ再レンダリング
};
export default Child1;
import { useContext, useMemo } from "react";
import { ValueContext } from "./Context.tsx";
// Child2コンポーネント
const Child2 = () => {
const { value3, updateValue3 } = useContext(ValueContext);
return useMemo(() => { // React要素をメモ化
console.log("re-rendered Child2");
return (
<div>
<h2>Child2</h2>
<p>value3: {value3}</p>
<button type="button" onClick={() => {
updateValue3(value3 + 1);
}}>+</button>
</div>
);
}, [value3]); // value3が更新されたときのみ再レンダリング
};
export default Child2;
実行結果
値が変更されたコンポーネントのみ再レンダリングが行われています。正確に言うとコンポーネントの関数自体は実行されていますが、useMemoの第2引数の値が変わらない限りレンダリングの再計算が走らないためパフォーマンスの改善が見込めます。
おわりに
自分自身まだまだReact初心者なので、もし間違っている点や直した方がいい記述があればぜひコメントしていただけると助かります。
関連する記事