memo
で包んだコンポーネントは、受け取るprops
が変わらないかぎり再レンダーされません。
本稿はReact公式サイト「memo」にもとづき、memo
はどう使うのか、およびどのような場合に使うとよいのかを解説します。説明内容と順序は、公式ドキュメントにしたがいました。ただし、解説はわかりやすく改め、またコード例とサンプル(StackBlitz)はTypeScriptを加えたうえで修正した部分が少なくありません。
構文
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
コンポーネントをmemo
でラップすると、メモ化されたコンポーネントが返されます。メモ化されたコンポーネントは、親コンポーネントのレンダリングにかかわらず、受け取るprops
が変わらないかぎり通常再レンダーされません。ただし、Reactがそれでもレンダリングすることはあり得ます。メモ化はパフォーマンス最適化のひとつであって、保証まではしません。
import { memo } from 'react';
const SomeComponent = memo((props) => {
});
memo
の構文はつぎのとおりです。
memo(Component, arePropsEqual?)
引数
-
Component
: メモ化するコンポーネント。- コンポーネントそのものは変更されません。返されるのはメモ化された新たなコンポーネントです。
- コンポーネントは関数や
forwardRef
によるものも含まれ、任意の有効なReactコンポーネントが渡せます。
-
arePropsEqual
(省略可能): 直前と今回のふたつのprops
を引数にとる関数。戻り値はprops
が変わったかどうかを示すboolean
値です。-
true
の場合:props
は変わっていないと評価し、キャッシュされたコンポーネントのまま、再レンダーしません。つまり、コンポーネントの出力も動きも直前と同じです。 -
false
の場合: 直前とprops
が変わったと評価され、コンポーネントは再レンダーされまます。 - 通常は、この関数を与える必要はありません。デフォルトで、ふたつの
props
の値それぞれがObject.is
により比べられます。
-
戻り値
第1引数のコンポーネントをメモ化した新たなReactコンポーネントです。
- 動作は第1引数に渡したコンポーネントと変わりません。
- ただし、親が再レンダーされたとき、渡された
props
が直前と同じかどうかを確かめます。変わらなければ、Reactはコンポーネントを再レンダーしません。
使い方
受け取ったprops
が直前と変わっていなければコンポーネントを再レンダーしない
Reactはデフォルトでは、親コンポーネントのレンダリングが起こると、つねに子を再起的に再レンダーします。memo
が返すのは、無駄な再レンダーを省くメモ化された新たなコンポーネントです。Reactは、コンポーネントの受け取ったprops
が、直前と変わっていないかを確かめます。同じであれば、再レンダーしません。
コンポーネントをmemo
で包むと、返されるのがメモ化されたコンポーネントです。戻り値は、もとのコンポーネント(第1引数)と同じように使えます。
export const Greeting: FC<Props> = memo(({ name }) => {
return <h1>Hello, {name}!</h1>;
});
Reactコンポーネントのレンダーロジックは、つねに純粋に保たなければなりません。props
や状態およびコンテクストが変わらないかぎり、つねに同じ出力を返すということです。memo
を用いることにより、Reactにコンポーネントが純粋であると伝わります。props
が直前と変わらなければ、Reactはコンポーネントを再レンダーしません。ただし、コンポーネントの状態や、用いたコンテクストが更新されたときは、再レンダリングが起こります。
つぎのコード例で、モジュールsrc/Greeting.tsx
のコンポーネント(Greeting
)はmemo
で包みました。したがって、渡されるプロパティname
が変わらなければ再レンダーされません。レンダリングされているか確認するため、console.log
を加えました。
import { memo } from 'react';
import type { FC } from 'react';
type Props = {
name: string;
};
export const Greeting: FC<Props> = memo(({ name }) => {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
return (
<h3>
Hello{name && ', '}
{name}!
</h3>
);
});
親コンポーネント(App
)のモジュールsrc/App.tsx
のコードはつぎのとおりです。状態変数はname
とaddress
のふたつで、テキストフィールド(<input>
要素)への入力により値が変わります。子コンポーネントGreeting
に渡したプロパティは、ふたつのうちのname
です。
import { useState } from 'react';
import { Greeting } from './Greeting';
function App() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
return (
<>
<label>
Name{': '}
<input
value={name}
onChange={({ target: { value } }) => setName(value)}
/>
</label>
<label>
Address{': '}
<input
value={address}
onChange={({ target: { value } }) => setAddress(value)}
/>
</label>
<Greeting name={name} />
</>
);
}
export default App;
つぎのサンプル001で試して、コンソール出力を確かめてみてください。name
のテキストフィールドに入力すると、子コンポーネントGreeting
は再レンダーされるでしょう。けれど、address
の値は、Greeting
のプロパティに含まれません。address
のテキストフィールドに入力しても、子コンポーネントの再レンダーは省かれるのです。
サンプル001■React + TypeScript: memo 01
[注記] memo
はパフォーマンスを最適化するためにお使いください。memo
なしにはコードが動かないという場合は、まず原因を探って修正するべきです。そのうえで、必要があればmemo
を用いましょう。
memo
はどこに使うべきか
メモ化はどこに用いるべきで、どういうときは不要か、大まかに比べるなら大きくふたつです。
- インタラクションがさほど多くなければ、
memo
を使わななくて構いません。- ページの切り替えや画面の一部の変更で済む場合。
- 細かなインタラクションが頻繁に生じるときは、メモ化は有効でしょう。
- 描画エディターのアプリケーションで、図形を移動させるなど。
memo
による最適化が役立つために必要な条件は、つぎのふたつです。
- コンポーネントがまったく同じ
props
で頻繁に再レンダーされる。 - 再レンダーするロジックの負荷が高い。
コンポーネントが再レンダーされても遅れはとくに感じられない場合、memo
は要りません。さらに、レンダー中に定めたオブジェクトや関数をそのまま渡していると、コンポーネントが受け取ったprops
は毎回異なると評価され、memo
は無意味です。useMemo
やuseCallback
と併せて使うことをお考えください。
こうした場合以外は、コンポーネントをmemo
でラップしても、効果はないでしょう。かといって、問題となることも考えにくいです。とりあえず、メモ化してしまうという開発チームもあります。デメリットとしては、コードが読みにくくなることです。また、レンダーごとに変わる値がひとつでもコンポーネントに含まれていれば、メモ化する意味はありません。
不要なメモ化を避けるための5つの原則
メモ化はつねに有効とはかぎりません。つぎの5つの原則にしたがえば、不要なメモ化が避けられるでしょう。
- コンポーネントが子コンポーネントを視覚的にラップするときは、子のJSXは
children
として受け取るようにします。子を包む親コンポーネントが自身の状態を更新しても、Reactは子の再レンダーは要らないとわかるからです。 - 状態はできるだけローカルに持ち、無闇にコンポーネントツリー上を引き上げないようにします(「React + TypeScript: コンポーネント間で状態を共有する」参照)。フォーム入力や項目へのホバーといった状態は、頻繁に変わるインタラクションです。ツリーのトップやグローバルの状態ライブラリに保持しないでください。
- レンダーロジックを純粋に保ちましょう。コンポーネントの再レンダーで意図しない動きになったり、表示に明らかな問題が起こるとすれば、それはバグです。メモ化で避けようとするのでなく、原因をつきとめて修正してください。
- 不必要に状態を更新するエフェクトは避けましょう。Reactアプリケーションでパフォーマンスの問題を引き起こす原因の多くが、エフェクトによる連鎖的な状態の更新です。コンポーネントはそのたびに再レンダーされてしまいます。
- エフェクトから要らない依存値は除いてください。メモ化しなくても、オブジェクトや関数をエフェクトの中あるいは外に移すだけで、簡単に解決できることもあります。
以上の原則にしたがっても、遅いと感じるインタラクションが残るかもしれません。そのような場合には、React Developer ToolsのProfilerパネルをお使いください。どのコンポーネントをメモ化すればもっとも効果的かが確かめられます。このように開発を進めることで、デバッグしやすく、わかりやすいコードが書けるでしょう。React公式サイトが推奨する理由です。将来に向けては、自動的にメモ化する研究も進められています(「React without memo」参照)。
状態が変わるとメモ化されたコンポーネントは更新される
メモ化されたコンポーネントが確かめるのは、親から渡されたprops
の直前との違いです。props
とは別に、自身の状態が変われば再レンダーされます。
前掲サンプル001のモジュールsrc/Greeting.tsx
のコンポーネント(Greeting
)にuseState
で状態変数(greeting
)を加えました。さらに、その値は新たに定める子コンポーネント(GreetingSelector
)にプロパティ(value
)として渡しています。状態変数値を変える(setGreeting
)のは、子コンポーネントです。
// import { memo } from 'react';
import { memo, useState } from 'react';
import { GreetingSelector } from './GreetingSelector';
export const Greeting: FC<Props> = memo(({ name }) => {
const [greeting, setGreeting] = useState('Hello');
return (
<>
<h3>
{/* Hello{name && ', '} */}
{greeting}
{name && ', '}
{name}!
</h3>
<GreetingSelector value={greeting} onChange={setGreeting} />
</>
);
});
モジュールsrc/GreetingSelector.tsx
のコンポーネント(GreetingSelector
)は、親から状態変数値value
とその設定関数onChange
をプロパティとして受け取ります。設定を変えるのはふたつのラジオボタン(<input type="radio"
)です。
import type { FC } from 'react';
type Props = {
onChange: (greeting: string) => void;
value: string;
};
type InputItemmProps = Props & {
greeting: string;
text: string;
};
const InputItem: FC<InputItemmProps> = ({
onChange,
greeting,
text,
value,
}) => {
return (
<label>
<input
type="radio"
checked={value === greeting}
onChange={() => onChange(greeting)}
/>
{text}
</label>
);
};
export const GreetingSelector: FC<Props> = ({ value, onChange }) => {
return (
<>
<InputItem
onChange={onChange}
greeting="Hello"
text="Regular greeting"
value={value}
/>
<InputItem
onChange={onChange}
greeting="Hello and welcome"
text="Enthusiastic greeting"
value={value}
/>
</>
);
};
コンポーネントGreetingSelector
のラジオボタンを切り替えると、親のGreeting
の状態(greeting
)が変わります。greeting
はmemo
で包まれており、受け取るprops
は変わらなくとも、状態が更新されればコンポーネントは再レンダーされるのです(サンプル002)。
サンプル002■React + TypeScript: memo 02
ただし、状態設定関数に現行と同じ値を渡して呼び出した場合は、コンポーネントのメモ化にかかわらず、再レンダーされません。関数コンポーネントは呼び出されても、レンダリングが省かれるのです。
コンテクストが変わるとメモ化されたコンポーネントは更新される
メモ化されたコンポーネントが確かめるのは、親から渡されたprops
の直前との違いです。props
とは別に、使っているコンテクストが変われば再レンダーされます。
つぎのモジュールsrc/App.tsx
は、createContext
でコンテクスト(ThemeContext
)をつくっています。プロバイダにvalue
として与えるのは状態変数値theme
です(ボタンクリックで値は切り替わります)。子コンポーネントGreeting
に渡すプロパティのname
は決め打ち("Taylor"
)なので、値が変わることはありません。
import { createContext, useCallback, useState } from 'react';
import { Greeting } from './Greeting';
const defaultTheme = 'dark';
export const ThemeContext = createContext(defaultTheme);
function App() {
const [theme, setTheme] = useState(defaultTheme);
const handleClick = useCallback(() => {
setTheme(theme === 'dark' ? 'light' : 'dark');
}, [theme]);
return (
<ThemeContext.Provider value={theme}>
<button onClick={handleClick}>Switch theme</button>
<Greeting name="Taylor" />
</ThemeContext.Provider>
);
}
export default App;
モジュールsrc/Greeting.tsx
の子コンポーネント(Greeting
)は、useContext
で取り出したコンテクストの値(theme
)に応じて、要素のクラス(className
)を切り替えています(要素のカラー変更)。コンポーネントはmemo
で包みました。
import { memo, useContext } from 'react';
import type { FC } from 'react';
import { ThemeContext } from './App';
type Props = {
name: string;
};
export const Greeting: FC<Props> = memo(({ name }) => {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
const theme = useContext(ThemeContext);
return <h3 className={theme}>Hello, {name}!</h3>;
});
つぎのサンプル003で試すと、親コンポーネントの[Switch theme]ボタンをクリックするたびに、子のGreeting
は再レンダーされます(コンソール出力をお確かめください)。前述のとおり、Greeting
はメモ化し、渡されるプロパティ(name
)も変わりません。けれど、コンテクストの値(theme
)が切り替わっています。すると、それを用いるコンポーネントは再レンダーされるのです。
サンプル003■React + TypeScript: memo 03
開発が進むにつれ、コンテクストのもつ値は膨らんでくるかもしれません。すると、子コンポーネントの再レンダーは、コンテクストの特定の値が変わったときに留めたい場合もあるでしょう。そういうときは、子コンポーネントを、新たにつくる親でラップしてください。useContext
は親コンポーネントから呼び出すのです。そのうえで、子コンポーネントに必要な値は、それぞれプロパティとして与えます。こうすれば、コンポーネントのメモ化も働くでしょう。具体的なコードとサンプルについては、「子コンポーネントを包んだ親からuseContextを呼び出す」をお読みください。
props
はできるだけ変えない
memo
で包んだコンポーネントは、受け取ったprops
のいずれかが浅い(shallow)比較で直前と異なるとき再レンダーされます。Reactがコンポーネントのprops
すべてを前回の値と比べるとき用いるのはObject.is
です。とくに、オブジェクトは、プリミティブ値と異なり、参照が比較されることにご注意ください。
-
Object.is(3, 3)
:true
-
Object.is({}, {})
:false
useMemo
でprops
の値を無駄につくり直さない
memo
の有効性を高めるには、子が受け取るprops
の値はできるだけ変えないようにします。親コンポーネントが値をuseMemo
に包んで渡せば、オブジェクトであっても無駄につくり直されません。
const Page: FC = () => {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
const person = useMemo(
() => ({ name, age }),
[name, age]
);
return <Profile person={person} />;
}
const Profile: FC<Props> = memo(({ person }) => {
});
必要な値だけをprops
として渡す
可能であれば、必要な値だけを子にprops
として与えることです。使う値がオブジェクトの中の一部なら、丸ごとprops
で渡すことはありません。
const Page: FC = () => {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}
const Profile: FC<Props> = memo(({ name, age }) => {
});
props
を更新の少ない値に変換して渡す
親からprops
で受け取ったオブジェクトそのものを使うのでなく、処理や評価した結果がほしいという場合もあるでしょう。親コンポーネントの側でできるのなら、結果だけ渡せばprops
の更新は減らせるかもしれません。
つぎのコード例では、子コンポーネントが受け取るのは、頻繁に変わるかもしれない値ではなく、値があるかどうかを示すboolean
値(hasGroups
)です。
const GroupsLanding: FC<Props> ({ person }) => {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}
const CallToAction: FC<CallToActionProps> = memo(({ hasGroups }) => {
});
props
で渡す関数の更新を省く
コンポーネントの中で定義された関数は、デフォルトではオブジェクトと同じように、レンダリングのたびにつくり直されます。メモ化された子コンポーネントにprops
で関数を渡す場合には、無駄な更新は避けなければなりません。考えられるのは、つぎのふたつの対応です。
- 関数をコンポーネントの外で定めてください。
- コンポーネント外の関数は、再レンダリングでつくり直されません。
- コンポーネント内の関数定義を
useCallback
で包みましょう。- 依存配列が変わらないかぎり、関数は再定義されません。
memo
の第2引数にカスタム比較関数を定める
子コンポーネントが渡されたprops
を直前と比べるとき、デフォルトの浅い比較(Object.is
)では足りないことがありえます。その場合、memo
の第2引数として渡せるのがカスタム比較関数です。比較関数の受け取るふたつの引数がそれぞれ前回と今回のprops
、ふたつに変わりがないかをboolean
値で返します。値が同じと評価するなら戻り値はtrue
で、再レンダーされません。
const Chart: FC<Props> = memo(({ dataPoints }) => {
// ...
}, arePropsEqual);
const arePropsEqual = (oldProps: Props, newProps: Props) => {
return (
oldProps.dataPoints.length === newProps.dataPoints.length &&
oldProps.dataPoints.every((oldPoint, index) => {
const newPoint = newProps.dataPoints[index];
return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
})
);
}
memo
に第2引数で比較関数を与えた場合、効果はブラウザのデベロッパーツールで[パフォーマンス]パネルからお確かめください。なお、React は本番モードで動作させましょう。
カスタム比較関数を使う場合の注意
memo
の第2引数(arePropsEqual
)にカスタム比較関数を与えた場合、props
の値はオブジェクトだけでなく、関数も含めてすべて前回と正しく比べなければなりません。
関数で注意すべきことは、親コンポーネントのprops
と状態をクロージャに閉じ込めることです。クロージャ内が異なるのに比較結果の戻り値をtrue
にすると、きわめて見つけにくいバグの原因となります。たとえば、実際にはoldProps.onClick !== newProps.onClick
なのにtrue
を返すと、コンポーネントはonClick
ハンドラ内で以前のレンダー時のprops
と状態を「見続ける」ことになるからです。
props
のデータ構造の深さが100%わかっている場合以外、カスタム比較関数(arePropsEqual
)で等価性を調べるのはお勧めしません。等価性を深く確かめようとすることは過剰な負荷につながります。あとでデータ構造が変わったら、数秒間フリーズしてしまうかもしれません。
トラブルへの対応
props
に渡しているのは同じ内容のオブジェクト・配列・関数なのにメモ化したコンポーネントが再レンダーされる
Reactが前回と今回のprops
が等しいかを調べるのは浅い比較です。つまり、親コンポーネントが再レンダーのたびにオブジェクトや配列をつくり直せば、中身のプロパティや要素が同じであっても、Reactは参照にもとづいて異なると評価します。また、コンポーネント内に定められた関数も、デフォルトではレンダー時に再定義されるので同じとはみなされません。これを避けるためには、前述「props
はできるだけ変えない」をご参照ください。