【React】メモ化する
はじめに
ページ内に「A」と「B」の2つの入力項目があります。
「A」と「B」の値に依存関係はなく、親コンポーネントで定義した1つのstateで管理しています。
このとき、「A」の値を変更すると、「B」の再レンダリングが行われます。
「B」は何も変更していないのに。
開発環境
Windows 10
TypeScript 4.9.5
React 18.2.0
Visual Studio 2022
サンプルコード
冒頭のページは以下のようなコードになっていることとします。
import { useState } from 'react';
type Values = {
valueA: string;
valueB: number;
}
const Parent = () => {
const [values, setValues] = useState<Values>({
valueA: '',
valueB: 0
});
const setValueA = (value: string) => {
setValues({ ...values, valueA: value });
}
const setValueB = (value: number) => {
setValues({ ...values, valueB: value });
}
return (
<div>
<ChildA value={values.valueA} setValue={setValueA} />
<ChildB value={values.valueB} setValue={setValueB} />
</div>
);
}
type PropsA = {
value: string;
setValue: (value: string) => void;
}
const ChildA = (props: PropsA) => {
console.log('ChildA');
return (
<input
type='text'
value={props.value}
onChange={event => props.setValue(event.target.value)}
/>
);
}
type PropsB = {
value: number;
setValue: (value: number) => void;
}
const ChildB = (props: PropsB) => {
console.log('ChildB');
return (
<input
type='number'
value={props.value}
onChange={event => props.setValue(event.target.value)}
/>
);
}
親コンポーネントで定義したvalues
というstateで「A」「B」それぞれの値valueA
、valueB
を管理しており、子コンポーネントのpropsのvalue
に渡しています。
また、子コンポーネントには値を変更するための関数をsetValue
として渡しており、各子コンポーネント内のinput
のonChange
でこれを実行しています。
加えて、レンダリングが行われたことがわかるよう、各子コンポーネントの先頭にconsole.log
を書いています。
上記のサンプルコードでは、「A」のテキストボックスに文字を入力するたびに、
> childA
> childB
の2行セットが毎回コンソールに出力されます。
要は、1文字入力するたびにすべての子コンポーネントがレンダリングされている訳です。
このサンプルでは入力項目が2つしかなく、体感では何も気になりません。
しかし、同じ構成で子コンポーネントを数百個配置した場合、1文字入力するたびに画面が固まったようになります。
メモ化
できれば、入力していない項目のレンダリングは控えたいです。
それを実現するために、reactでは「メモ化」という機能があります。
「メモ化」とは、入力情報が変わらない場合に前回処理した結果をそのまま使う機能です。
React.memo
さっそくサンプルコードをメモ化してみます。
まずは子コンポーネントChildA
から。
+ import * as React from 'react';
type PropsA = {
value: string;
setValue: (value: string) => void;
}
+ const ChildA = React.memo<PropsA>((props: PropsA) => {
- const ChildA = (props: PropsA) => {
console.log('ChildA');
return (
<input
type='text'
value={props.value}
onChange={event => props.setValue(event.target.value)}
/>
);
+ });
- }
ChildA
全体をReact.memo
で囲みました。
これによって、「props
が同じなら、前回レンダリングしたChildA
の結果をそのまま使う」ことができます。
ChildB
にも同様の変更を行うことで、「A」に文字入力したとき、
> childA
だけがコンソール出力され、「B」のレンダリングが行われなくなる・・・
はずですが、これだけではまだ
> childA
> childB
がコンソール出力されてしまいます。
原因は
onChange={event => props.setValue(event.target.value)}
でprops.setValue
に毎回"異なる"関数が渡されるため、「props
が異なる」と判断されないためです。
use.Callback
props.setValue
も毎回"同じ"関数を渡すため、親コンポーネントでuseCallback
を宣言します。
useCallback
は関数を丸ごとメモ化するイメージで、第2引数の配列に指定されたいずれかのパラメータに変更がなければ関数の再評価を行わない(=前回と"同じ"関数を返す)という、reactの標準フックです。
+ import { useState, useCallback } from 'react';
- import { useState } from 'react';
type Values = {
valueA: string;
valueB: number;
}
const Parent = () => {
const [values, setValues] = useState<Values>({
valueA: '',
valueB: 0
});
+ const setValueA = useCallback((value: string) => {
- const setValueA = (value: string) => {
setValues({ ...values, valueA: value });
+ }, [values]);
- }
+ const setValueB = useCallback((value: number) => {
- const setValueB = (value: number) => {
setValues({ ...values, valueB: value });
+ }, [values]);
- }
return (
<div>
<ChildA value={values.valueA} setValue={setValueA} />
<ChildB value={values.valueB} setValue={setValueB} />
</div>
);
}
これで無事に、「A」に文字入力したとき
> childA
だけがコンソール出力されるようになります。
propsが"同じ"とは?
今回の場合、子コンポーネントは親から与えられたpropsが"同じ"ときReact.memo
の機能によって再レンダリングされなくなりました。
では、propsが"同じ"とはどういう状況でしょうか。
ここまでの例で、ChildA
は親からvalue
というstring型の値とsetValue
という関数を受け取っています。
setValue
についてはuseCallback
で"同じ"関数が渡されることになったので良いですが、value
は何をもって"同じ"と判定されるのか、が問題です。
実は、内部の判定ではObject.is
がTrueのときに"同じ"と判断されるようです。
プリミティブ型であれば、同値であれば"同じ"になります。
オブジェクト型であれば「浅い比較」がTrueであれば"同じ"になります。
今回のvalue
はプリミティブ型なのであまり考えなくて良いですが、これがオブジェクト型だった場合、見た目は同じでも"異なる"と判断され、毎回レンダリングが発生することになります。(私はここではまりました)
オブジェクト型を「深い比較」で比較したい場合は、独自の比較関数を定義する必要があります。arePropsEqual
に、"同じ"ならTrueを返す独自の比較関数を割り当てることで対応できるようです。
Propsに関数を指定するときの注意
上記のサンプルコードで
setValue={setValueA}
の箇所は「setValue
に関数setValueA
自体を指定している」という意味になります。
もしこれが
setValue={setValueA()}
とか書いてしまうと「setValue
に関数setValueA
の結果を指定している」という意味になり、コンパイルエラーとなります。
メモ化 不要論
ちまたでは「memoは使わなくても良い」という記事もあります。
一方で「とりあえず全部メモ化せよ」という意見もあります。
私見としては、
- (子コンポーネント)外からどのように参照されたとしても、自分自身はメモ化する準備をしておく。
- (親コンポーネント)子コンポーネントでメモ化の準備が行われているかもしれないので、とりあえずそっちに合わせる。
- (親コンポーネント)今は少ない子コンポーネント数だからメモ化しなくてもあまり影響はないが、今後の機能拡張で子コンポーネントがどれくらい増えるかわからない。
等の理由から「とりあえず全部メモ化せよ」に1票って感じです。
ただし、せっかくメモ化するなら意図したように動くよう、propsなり第2引数なりに正しいパラメータを指定するよう気を付けたいです。
また、サンプルコードにも書いたようにconsole.log
等を通じて意図したとおりにレンダリングが行われているかの確認も必要と思います。
以上です。
P.S.
別業務(とDiablo4)で1ヵ月ほどプログラミングから離れていました。
久々にコーディング再開しようと思ったら、どこまで進んでいて、何が保留になっていて、この先どう進めようとしていたかがまったく思い出せませんでした。
メモ、大事ですね。
Discussion