はじめに
現在所属しているチームにて、専門知識と英語力の向上を目指して英語の参考資料をもとに勉強会を実施しています。
その中で、Reactの再レンダリングに関する記事を参考資料とした扱った際の勉強会で学ぶことが多くありました。
参考資料は以下です。
フックやAPI別にアンチパターンと最適化されたパターンを紹介しています。
※ 本記事で記載されている、引用文・サンプルコードのリンクについては全て上記の参考資料で使用されているものです。
本記事では参考記事よりコンテクストの2つのパターンを取り上げ再レンダリングへの理解を深め、英語の記事を読む重要性を理解することを目的とします。
本記事の目的とゴール
- 英文の記事を読むことの意義を理解する
- コンテクストを使用した際の再レンダリングの最適化について理解を深める
英語の記事を読むこと
英語を業務で使うようになって改めて気づいたことは、ドキュメントや有益な1次情報は英語の記事が圧倒的に多いというこです。
例えば、現状使用しているものを挙げると、
- Formik
- Testing Library
- Emotion
これらのドキュメントは全て英語で書かれています。
ドキュメントに関しては、公式が日本語訳を提供しているものも存在しますが、それは一部だけで基本的には英語で書かれています。
また、1次情報に関してもGitHubのIssuesだったり、Stack Overflowだったり、今回の参考資料のような記事だったりと有益な情報は英語で書かれているものが多いです。
また、チームのメンバーからも「基本的に技術情報の検索は英語で検索する方が有益な情報に辿り着ける」というアドバイスも貰いました。
ドキュメントは日本語では伝わりづらい詳細なニュアンスで書かれていたいりします。
そして、そういった部分に重要なことが書かれている印象があります。
つまり、英語の記事を読むことに慣れることでドキュメントをより理解できると考えています。
それゆえに、英語の記事を時間がかかっても粘り強く読むことが大切だと実感しました。
コンテクストによる再レンダリングを防ぐ(Preventing re-renders caused by Context)
参考記事より、以下の2つのパターンを取り上げます。
- プロバイダーのvalueをメモ化する(Preventing Context re-renders: memoizing Provider value)
- コンテクストセレクター ~高階コンポーネントとReact.memoの組み合わせ~(Preventing Context re-renders: Context selectors)
それぞれのパターンの説明、アンチパターン・最適化されたコードをもとに、どのような仕組みになっているのかを確認します。
プロバイダーのvalueをメモ化する
Preventing Context re-renders: memoizing Provider valueの項目にて以下の説明がされています。
If Context Provider is placed not at the very root of the app, and there is a possibility it can re-render itself because of changes in its ancestors, its value should be memoized.
翻訳は以下です。
もしコンテクストプロバイダーがアプリのルートに配置されていない場合、それ(先祖コンポーネント)の変化により自身が再レンダリングされる可能性がある、その値(Contextプロバイダーの値)はメモ化されるべきである。
当初は、「at the very root of the app」の部分を「アプリの大きな原因となって」と解釈してしまい、辻褄が合わなくなりました。再度調べてみると、単純に「根幹」という意味で良いことが判明し、辻褄が合う翻訳ができました。このあたりのニュアンスの違いを理解することで、文章がどのようなことを述べているのかを正確に解釈できるのではと実感しました。
アンチパターンの例は以下です。
上記のコードはボタンをクリックすることにより、以下の流れで子コンポーネント(Child2)に余計なレンダリングが発生します。
- Appのstateが更新される
- Providerが更新される
- Provider2が更新される
- Child2が再レンダリングされる
原因は3にてProvider2内のvalue変数がuseMemoでメモ化されていないことです。
Appが再レンダリングされるたびに、Provider2内のvalueが新たにオブジェクトを生成します。
そのvalueをChild2が受け取るため、再レンダリングが発生します。
最適化されたコードは以下です。
import React, {
useState,
createContext,
useContext,
useMemo,
ReactNode,
} from "react";
const Context = createContext<{ value: number }>({ value: 1 });
const Context2 = createContext<{ value: number }>({ value: 1 });
const Provider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
};
const value = useMemo(() => ({ value: state }), [state]);
return <Context.Provider value={value}>{children}</Context.Provider>;
};
const Provider2 = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
};
const value = useMemo(() => ({ value: state }), [state]); // メモ化
return <Context.Provider value={value}>{children}</Context.Provider>;
};
const useValue = () => useContext(Context);
const useValue2 = () => useContext(Context2);
const Child1 = () => {
const { value } = useValue();
const { value: value2 } = useValue2();
console.log("Child1 re-renders: ", value, value2);
return <></>;
};
const Child2 = () => {
const { value } = useValue();
const { value: value2 } = useValue2();
console.log("Child2 re-renders", value, value2);
return <></>;
};
const Child1Memo = React.memo(Child1);
const Child2Memo = React.memo(Child2);
const App = () => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
};
return (
<Provider>
<Provider2>
<h2>Open console, click a button</h2>
<p>
Children will unnecessary re-render because of the second provider,
which doesn't memoize value
</p>
<button onClick={onClick}>button {state}</button>
<Child1Memo />
<Child2Memo />
</Provider2>
</Provider>
);
};
export default App;
上記でも述べたとおり、Provider2のvalue変数をuseMemoでメモ化することにより、Child2の再レンダリングを防いでいます。
これにより、メモ化されたvalueが変更されない限り子コンポーネントへの再レンダリングは発生しなくなります。
コンテクストセレクタ(高階コンポーネントとReact.memoの組み合わせ)
参考資料のPreventing Context re-renders: Context selectorsでは、コンテクストの値の一部を使用するコンポーネントの再レンダリングを防ぐパターンを紹介しています。
詳細は以下です。
There is no way to prevent a component that uses a portion of Context value from re-rendering, even if the used piece of data hasn’t changed, even with useMemo hook.
Context selectors, however, could be faked with the use of higher-order components and React.memo.
上記は以下のことを述べています。
コンテクストセレクタの値の一部を使うコンポーネントに対して再レンダリングを防ぐ方法は存在しない。データの一部が変更されない、またはuseMemoフックを用いても、再レンダリングを防ぐことはできない。
ただし、コンテクストセレクタは高階コンポーネントとReact.memoを使って模倣することができる。
アンチパターンのコードは以下です。
import { useState, createContext, useContext, useMemo, ReactNode } from "react";
const Context = createContext<{ value: number; staticValue: string }>({
value: 1,
staticValue: "",
});
const Provider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState(1);
const onClick = () => {
setState(state + 1);
};
const value = useMemo(
() => ({
value: state,
staticValue: "1",
}),
[state]
);
return (
<Context.Provider value={value}>
<button onClick={onClick}>click here</button>
{children}
</Context.Provider>
);
};
const useValue = () => useContext(Context);
const Child1 = () => {
const { value } = useValue();
console.log("Child with dynamic value re-renders: ", value);
return <>{value}</>;
};
const Child2 = () => {
const { staticValue } = useValue();
console.log("Child with static value re-renders", staticValue);
return <>{staticValue}</>;
};
const App = () => {
return (
<Provider>
<h2>Open console, click a button</h2>
<p>Only child with dynamic content will re-render</p>
<p>Child that uses context "selector" won't</p>
<Child1 />
<Child2 />
</Provider>
);
};
export default App;
上記のコードはボタンをクリックすると、stateの更新に伴い、staticValueをコンテクストの値として受け取っている関係のないChild2も再レンダリングされてしまいます。
ボタンがクリックされるとProviderが再レンダリングされ、これによりコンテクストが更新されたと判断され、Child2でも再レンダリングが発生します。
原因は子コンポーネント(Child2)を「コンテクストから値を受け取るコンポーネント」「その受け取った値を使用するメモ化されたコンポーネント」と役割ごとにコンポーネントを分割していないことです。
最適化された例は以下です。
const withStaticValueFromContext = (
Component: ({ staticValue }: { staticValue: string }) => JSX.Element
) => {
const ComponentMemo = React.memo(Component);
return () => {
const { staticValue } = useValue();
return <ComponentMemo staticValue={staticValue} />;
};
};
const Child2 = ({ staticValue }: { staticValue: string }) => {
console.log("Child with static value re-renders", staticValue);
return <>{staticValue}</>;
};
const Child2WithStaticValue = withStaticValueFromContext(Child2);
大きな変更点は上記で、React.memoと高階コンポーネントを使用して最適化を実施しています。
まず、コンポーネントをwithStaticValueFromContextとChild2の2つに分割しています。
外側のコンポーネント(withStaticValueFromContext)では、コンテクストから必要な情報(変数staticValue)を受け取ります。
それをメモ化された子コンポーネント(ComponentMemo)にpropsとして渡しています。
React.memoはwithStaticValueFromContextの引数として受け取ったコンポーネントに適用され、Child2WithStaticValue変数で高階コンポーネントを実行しています。
React.memoと高階コンポーネントについて
React.memoは、親コンポーネントから受け取るpropsに変更があった際にのみ再レンダリングされる特徴があり、今回のケースはこの特徴を利用しています。
staticValueが変更された際にのみ、Child2が再レンダリングされるようになっています。
ただし、React.memoだけでは最適化を実現できません。
それは、高階コンポーネントを組みわせることによって実現できます。
高階コンポーネントは、別のコンポーネントを引数として受け取り、新しいコンポーネントを返す関数です。
今回のケースだと、React.memo適用後のコンポーネントを返す無名関数となっています。
これにより、関数コンポーネントととして使用することができます。
そしてreturn文の中で、コンテクストの値を受け取ることで最新の状態でのstaticValueを得ることができ、子コンポーネント(ComponentMemo)を返す仕組みになっています。
その結果、上記で述べた「staticValueが変更された際にのみ、Child2が再レンダリングされる」最適化を実現しています。
まとめ
参考記事をとおして、以下を学ぶことができたのでパフォーマンス改善の場面で役立たせたいと考えています。
- コンテクストが関わる際の再レンダリングの最適化パターンを理解することができた
- 特に曖昧だったReact.memoと高階コンポーネントの役割を理解できた
また、英語の記事を読むことで以下の学びがありました。
- 英語の記事を読むことへの心理的ハードルが下がる
- 専門分野でよく使われる英語表現を学ぶことができる
- 日本語では表現できない細かいニュアンスを学ぶことができる
- 英語の記事を読むことに慣れることで、ドキュメントをより理解できるようになる
英語に関してはまだまだ学習中なので記事を読むのにとても時間がかかりますが、今後も続けていきたいです。