はじめに
React(v16.12.0) の Context の更新による不要な再レンダリングを防ぐ方法についての備忘録です。
useContext
フックなどで利用する Context のデメリットとして
「Context を更新したら、その Context を利用しているコンポーネントがすべて再レンダリングされてしまう」
ということが記載されている時がありますが、関数コンポーネントであれば再レンダリングを防げます(クラスコンポーネントでもできるかも)。
ということで、この記事は関数コンポーネントを対象としています。
また、デモは CodeSandbox 上に置いてあります。編集して動作を確認してみると理解が深まると思います。
useContext を利用する上で、理解しておく必要がある概念(用語)
- Context(コンテキスト)
- Context オブジェクト
- Provider(プロバイダ)
- Consumer(コンシューマ)
Context(コンテキスト)
文脈によって意味合いが異なるが、React に関しては以下のいずれかを指していることが多い。
- Props を利用せずに様々な階層のコンポーネントに値を共有する React の仕組みや API のこと
- Context オブジェクトのこと
- Context オブジェクトの値のこと
「1.」の意味の場合、「React Context」、「React Context API」や「Context API」などと記載されていることもある。
「Context を利用する」とは
前述の「1.」の意味の「Context を利用する」という表現を、より具体的な表現にすると以下の通り。
- 「Context という React の仕組み(API)を利用して、Props を利用せずに様々な階層のコンポーネントに値を渡せるようにする」
- 「
React.createContext
で Context オブジェクトと Provider を定義し、useContext
で Consumer を定義して、Props を利用せずに様々な階層のコンポーネントに値を渡せるようにする」
Context オブジェクト
React.createContext
という React の API(メソッド)の戻り値。
Context オブジェクトとuseContext
を利用することで、Props を利用せずに様々な階層のコンポーネントに値を渡せる(関数コンポーネントの場合)。
Provider(プロバイダ)
Consumer に値を共有するコンポーネントのこと。Context オブジェクトが保持している。
「Provider コンポーネント」と記載されていることもある。
以下の場合、MyContext.Provider
コンポーネントが Provider に当たる。
import React, { createContext } from "react";
const MyContext = createContext();
export default function App() {
const name = "soarflat";
return <MyContext.Provider value={name}></MyContext.Provider>;
}
value
プロパティの値が Context オブジェクトが保持している値であり、useContext
を利用することで、この値を取得できる。
Consumer(コンシューマ)
useContext
などを利用して Context オブジェクトから値を取得するコンポーネントのこと。
「Consumer コンポーネント」と記載されていることもある。
以下の場合、Child1
コンポーネントが Consumer に当たる。
import React, { createContext, useContext } from "react";
const MyContext = createContext();
// Consumer
function Child1() {
const name = useContext(MyContext);
return <h1>{name}</h1>;
}
// Consumer ではない
function Child2() {
return <h2>Not Consumer</h2>;
}
export default function App() {
const name = "Consumer";
return (
<MyContext.Provider value={name}>
<Child1 />
<Child2 />
</MyContext.Provider>
);
}
Child2
コンポーネントは Context オブジェクトから値を取得していないので、Consumer ではない。
Provider 内のすべての Consumer は、Provider のvalue
プロパティが更新される度に再レンダリングされる
以下は Context(Context オブジェクトの値)の更新が原因で、不要な再レンダリングが発生しているデモ。
Provider 内のすべての Consumer は、Provider のvalue
プロパティ(Context オブジェクトの値)が更新される度に再レンダリングされる。
import React, { createContext, useContext, useReducer } from "react";
const CountContext = createContext();
function countReducer(state, action) {
switch (action.type) {
case "increment": {
return { count: state.count + 1 };
}
case "decrement": {
return { count: state.count - 1 };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function CountProvider({ children }) {
const [state, dispatch] = useReducer(countReducer, { count: 0 });
const value = {
state,
dispatch
};
return (
// value(state か dispatch のどちらか)が更新したら、
// CountContext.Provider 内のすべての Consumer が再レンダリングされる。
<CountContext.Provider value={value}>{children}</CountContext.Provider>
);
}
function Count() {
console.log("render Count");
// CountContext からは state のみを取得しているが、
// dispatch が更新されても再レンダリングされる
const { state } = useContext(CountContext);
return <h1>{state.count}</h1>;
}
function Counter() {
console.log("render Counter");
// CountContext からは dispatch のみを取得しているが、
// state が更新されても再レンダリングされる
const { dispatch } = useContext(CountContext);
return (
<>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
export default function App() {
return (
<CountProvider>
<Count />
<Counter />
</CountProvider>
);
}
Counter
コンポーネントが取得しているdispatch
は更新されることはないが、state
が更新される度にCounter
コンポーネントも再レンダリングされてしまう。
上記のデモは特に問題ないが、以下の場合、パフォーマンスの問題を引き起こす可能性がある。
- 不要に再レンダリングされる Consumer の数が多い
- 不要に再レンダリングされる Consumer や、その Consumer の子コンポーネントのレンダリングコストが高い
Context(Context オブジェクトの値)の更新による不要な再レンダリングを防ぐ方法
Context の更新による不要な再レンダリングを防ぐ方法は以下の3つ。
- Context(Context オブジェクト)を分割する
-
React.memo
を利用する -
useMemo
を利用する
Context(Context オブジェクト)を分割する
以下は Context を分割して不要な再レンダリングを防いでいるデモ。
import React, { createContext, useContext, useReducer } from "react";
const CountStateContext = createContext();
const CountDispatchContext = createContext();
function countReducer(state, action) {
switch (action.type) {
case "increment": {
return { count: state.count + 1 };
}
case "decrement": {
return { count: state.count - 1 };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function CountProvider({ children }) {
const [state, dispatch] = useReducer(countReducer, { count: 0 });
// CountStateContext.Provider の value が更新したら、
// CountStateContext の値を取得している全ての Consumer が再レンダリングされる。
// CountDispatchContext.Provider の value が更新したら、
// CountDispatchContext の値を取得している全ての Consumer が再レンダリングされる。
return (
<CountStateContext.Provider value={state}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
);
}
function Count() {
console.log("render Count");
// state と dispatch を保持する Context オブジェクトが異なるので、
// dispatch が更新されてもこのコンポーネントは再レンダリングされない。
const state = useContext(CountStateContext);
return <h1>{state.count}</h1>;
}
function Counter() {
console.log("render Counter");
// state と dispatch を保持する Context オブジェクトが異なるので、
// state が更新されてもこのコンポーネントは再レンダリングされない。
const dispatch = useContext(CountDispatchContext);
return (
<>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
export default function App() {
return (
<CountProvider>
<Count />
<Counter />
</CountProvider>
);
}
state
とdispatch
を保持する Context を分割したので、state
を更新してもCounter
コンポーネントは再レンダリングされない。
何らかの理由で Context を分割できない場合、後述のReact.memo
かuseMemo
を利用した方法で再レンダリングを防ぐ。
React.memo を利用する
以下のデモはReact.memo
を利用して不要な再レンダリングを防いでいるデモ。
import React, { createContext, useContext, useReducer } from "react";
const CountContext = createContext();
function countReducer(state, action) {
switch (action.type) {
case "increment": {
return { count: state.count + 1 };
}
case "decrement": {
return { count: state.count - 1 };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function CountProvider({ children }) {
const [state, dispatch] = useReducer(countReducer, { count: 0 });
const value = {
state,
dispatch
};
return (
// value(state か dispatch のどちらか)が更新したら、
// CountContext.Provider 内のすべての Consumer が再レンダリングされる。
<CountContext.Provider value={value}>{children}</CountContext.Provider>
);
}
function Count() {
console.log("render Count");
// CountContext からは state のみを取得しているが、
// dispatch が更新されても再レンダリングされる
const { state } = useContext(CountContext);
return <h1>{state.count}</h1>;
}
function Counter() {
console.log("render Counter");
// CountContext からは dispatch のみを取得しているが、
// state が更新されても再レンダリングされる
const { dispatch } = useContext(CountContext);
// CountContext.Provider の value の更新による Counter コンポーネントの
// 再レンダリングは避けられない。そのため、このコンポーネントは CountContext から値を
// 取得するだけにして、メモ化したコンポーネントに取得した dispatch を渡すようにする。
return <DispatchButton dispatch={dispatch} />;
}
// dispatch を Props として受け取るコンポーネントをメモ化し、不要な再レンダリングを防ぐ
const DispatchButton = React.memo(({ dispatch }) => {
console.log("render DispatchButton");
return (
<>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
});
export default function App() {
return (
<CountProvider>
<Count />
<Counter />
</CountProvider>
);
}
Context を分割していないため、state
を更新してもCounter
コンポーネントは再レンダリングされる。
そのため、Counter
コンポーネントはCountContext
の値を取得するだけにする。
そして、CountContext
の値を利用するコンポーネント(DispatchButton
)を切り出し、React.memo
でメモ化する。
このようにすれば、state
を更新してもDispatchButton
コンポーネントは再レンダリングされない。
useMemo を利用する
以下のデモはuseMemo
を利用して不要な再レンダリングを防いでいるデモ。
import React, { createContext, useContext, useReducer, useMemo } from "react";
const CountContext = createContext();
function countReducer(state, action) {
switch (action.type) {
case "increment": {
return { count: state.count + 1 };
}
case "decrement": {
return { count: state.count - 1 };
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function CountProvider({ children }) {
const [state, dispatch] = useReducer(countReducer, { count: 0 });
const value = {
state,
dispatch
};
return (
// value(state か dispatch のどちらか)が更新したら、
// CountContext.Provider 内のすべての Consumer が再レンダリングされる。
<CountContext.Provider value={value}>{children}</CountContext.Provider>
);
}
function Count() {
console.log("render Count");
// CountContext からは state のみを取得しているが、
// dispatch が更新されても再レンダリングされる
const { state } = useContext(CountContext);
return <h1>{state.count}</h1>;
}
function Counter() {
console.log("render Counter");
// CountContext からは dispatch のみを取得しているが、
// state が更新されても再レンダリングされる
const { dispatch } = useContext(CountContext);
// CountContext.Provider の value の更新による Counter コンポーネントの
// 再レンダリングは避けられない。そのため dispatch を利用するレンダリング結果(計算結果)を
// メモ化し、不要な再レンダリングを防ぐ。
return useMemo(() => {
console.log("rerender Counter");
return (
<>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}, [dispatch]);
}
export default function App() {
return (
<CountProvider>
<Count />
<Counter />
</CountProvider>
);
}
Context を分割していないため、state
を更新してもCounter
コンポーネントは再レンダリングされる。
そのため、useMemo
でCountContext
の値を利用するレンダリング結果をメモ化する。
このようにすれば、state
を更新してCounter
コンポーネントが再レンダリングされた時は、メモ化されたレンダリング結果を返す。
結果として、state
を更新しても不要な再レンダリングは発生しない。
終わり
Context は便利な API ですが、使い方によってはパフォーマンスの問題を引き起こす可能性があります。
そのため、再レンダリングが発生する条件と、再レンダリングを防ぐ方法を理解した上で利用しましょう。
本記事以外にも React に関連する記事を書いておりますので、興味があればそちらもどうぞ。
参考
お知らせ
Udemy で webpack の講座を公開したり、Kindle で技術書を出版しています。
Udemy:
webpack 最速入門(10,800 円 -> 2,000 円)
Kindle(Kindle Unlimited だったら無料):
React Hooks 入門(500 円)
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。