#はじめに
React.memo、useCallback、useMemoを利用したコンポーネント、関数、値のメモ化は、実装コストが少ない分「とりあえず記述しとけ!」みたいなノリで書きがちな初心者(自分)が多そうな気がします。
しかし、意味のないメモ化はただの負債でしかないので、ちゃんと理解した上で記述できるよう、おさえておきたい内容を初心者向けに記事にしました。
React.memo、useCallback、useMemoってなんや!?って方は、以前書いた記事を参考にしていただければと思います。
#JavaScriptのデータ型
まずおさえておきたいのは、JavaScriptのデータ型についてです。
JavaScriptのデータ型は基本型と参照型に分類されます。
- 基本型:number, string, boolean, synbol, null/undefined
- 参照型:array, object, function
これらは値を変数に格納する方法が異なります。
基本型の変数には値そのものが直接格納されますが、参照型の変数には値を格納しているメモリ上のアドレス(ポインタ、参照値)が格納されます。
#Reactのレンダリング
"Add a Boxer"でReact.memo
でメモ化したBoxer
コンポーネントを追加するケースを考えます。
const Boxer = React.memo(({ name, punch }) => {
console.log(`rendering ${name}`);
return <p onClick={() => punch(name)}>{name}</p>;
});
const App = () => {
const [boxers, setBoxers] = useState(['Ippo', 'Sendo', 'Mashiba']);
const punch = (name) => console.log(`${name} has punched`);
return (
<>
{boxers.map((name, i) => (
<Boxer key={i} name={name} punch={punch} />
))}
<button onClick={() => setBoxers([...boxers, prompt('Name a boxer')])}>
Add a Boxer
</button>
</>
);
};
コードを一見すると、Boxer
を追加したとき、既存コンポーネントのPropsであるname
とpunch
に変化はなさそうです。
メモ化が成功して、既存コンポーネントの再レンダリングは起こらないようにみえます。
この理由を考えるときに、前項の「JavaScriptのデータ型」を理解している必要があります。
Boxer
のPropsであるpunch
には関数が設定されています。
関数のデータ型は参照型であり、描画のたびに異なるインスタンスが生成されます(格納されるメモリのアドレスが変わります)。
結果、punch
の値が変化したとみなされて、Boxer
コンポーネントが再描画されることになるというわけです。
それでは、関数punch
の値が変化したとみなされないようにはどうすればいいのか...
ここで使用するのがuseCallback
です。
punch()
をuseCallback
でメモ化してみます。
const punch = useCallback((name) => console.log(`${name} has punched`), []);
再度Boxer
を追加してみると、今度は既存コンポーネントの再レンダリングが行われなくなりました。
#まとめ
React.memo
を使うときは、ラップするコンポーネントのPropsのデータ型に気をつけて、関数のような参照型が含まれる場合は必ずuseCallback
で合わせてメモ化しましょう!
#参考資料
JavaScript本格入門 山田祥寛 著
オブジェクト指向でなぜつくるのか 平澤章 著
Reactハンズオンラーニング Alex Banks, Eve Porcello 著