コンポーネントをmemoで包むと、親から渡されたプロパティ(props)が直前と変わらないかぎり再レンダーされません。ただし、コンテクストを使っていると(useContext)、propsは前と同じでも、コンテクストが更新されれば再レンダーされます。簡単なコード例で、その動きと対応方法を探ってみましょう。
ふたつの子コンポーネントでコンテクストを使う
つぎのモジュールsrc/App.tsxがつくるコンテクスト(createContext)は、ふたつの状態変数(nameとtheme)をvalueとして子のコンポーネントツリーに与えています。子コンポーネントは、NameEditorとGreetingのふたつです。あとに示すように、どちらもmemoでラップしてあります。なお、メモ化した子コンポーネントにプロパティとして渡す関数(handleChange)は、useCallbackに包みましょう。状態変数themeの値は[Switch theme]ボタン(<button>)で切り替わります。
import {
ChangeEventHandler,
createContext,
useCallback,
useState,
} from 'react';
import { Greeting } from './Greeting';
import { NameEditor } from './NameEditor';
const defaultContext = { name: 'Taylor', theme: 'dark' };
export const ThemeContext = createContext(defaultContext);
function App() {
const [theme, setTheme] = useState(defaultContext.theme);
const [name, setName] = useState(defaultContext.name);
const handleClick = useCallback(() => {
setTheme(theme === 'dark' ? 'light' : 'dark');
}, [theme]);
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
({ target: { value } }) => {
setName(value);
},
[name]
);
return (
<ThemeContext.Provider value={{ name, theme }}>
<button onClick={handleClick}>Switch theme</button>{' '}
<NameEditor onChange={handleChange} />
<Greeting />
</ThemeContext.Provider>
);
}
export default App;
モジュールsrc/Greeting.tsxのコンポーネントGreetingが、コンテクストから取り出す(useContext)のはふたつの値nameとthemeです。nameは表示するテキスト、themeはカラー設定のクラス(className)を変更します。
import { memo, useContext } from 'react';
import type { FC } from 'react';
import { ThemeContext } from './App';
export const Greeting: FC = memo(() => {
console.log('Greeting was rendered at', new Date().toLocaleTimeString());
const { name, theme } = useContext(ThemeContext);
return <h3 className={theme}>Hello, {name}!</h3>;
});
モジュールsrc/NameEditor.tsxのコンポーネントNameEditorが、コンテクストから得る(useContext)のがnameです。propsとして受け取ったonChangeで、値を入力フィールド(<input type="text">)のテキストに更新します。
import { memo, useContext } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import { ThemeContext } from './App';
type Props = {
onChange: ChangeEventHandler;
};
export const NameEditor: FC<Props> = memo(({ onChange }) => {
console.log('NameEditor was rendered at', new Date().toLocaleTimeString());
const { name } = useContext(ThemeContext);
return <input type="text" value={name} onChange={onChange} />;
});
つぎのサンプル001でお試しいただくと、とくに動きに問題はないでしょう。ふたつの子コンポーネントにはレンダリングを確かめるconsole.logが加えてあるので、コンソール出力をご覧ください。入力フィールドのテキストを編集すると、両コンポーネントが再レンダーされます。どちらも、更新されたnameの値を用いていますので、想定された結果です。
ところが、親コンポーネントの[Switch theme]ボタンをクリックすると、要素のカラーが変わるGreetingはよいとして、コンテクストのthemeなど使っていないNameEditorまでレンダリングされます。これは、コンポーネントの用いるコンテクストThemeContextが更新されたからです。
サンプル001■React + TypeScript: memo 04
子コンポーネントを包んだ親からuseContextを呼び出す
前掲サンプル001でしたら、子コンポーネントの描画がとくに重いとは感じません。けれど、コンテクストの抱える値が増え、子コンポーネントのレンダリング負荷も高まると、特定の値の更新時にのみ再レンダーしたいと考えるかもしれません。
そのような場合には、子コンポーネントは新たな親(Container)で包みましょう。
/* import { Greeting } from './Greeting';
import { NameEditor } from './NameEditor'; */
import { Container } from './Container';
function App() {
return (
<ThemeContext.Provider value={{ name, theme }}>
{/* <NameEditor onChange={handleChange} />
<Greeting /> */}
<Container onChange={handleChange} />
</ThemeContext.Provider>
);
}
そして、useContextはラップした親から呼び出してください。コンテクストから取り出した子コンポーネントが必要な値は、それぞれpropsとして与えるのです。
import { memo, useContext } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import { ThemeContext } from './App';
import { Greeting } from './Greeting';
import { NameEditor } from './NameEditor';
type Props = {
onChange: ChangeEventHandler<HTMLInputElement>;
};
export const Container: FC<Props> = memo(({ onChange }) => {
const { name, theme } = useContext(ThemeContext);
return (
<>
<NameEditor name={name} onChange={onChange} />
<Greeting name={name} theme={theme} />
</>
);
});
子コンポーネントは、もはやコンテクストを使いません。propsであれば、コンポーネントのメモ化が働きます。受け取った値が変わらないかぎり、再レンダーされません。
// import { memo, useContext } from 'react';
import { memo } from 'react';
// import { ThemeContext } from './App';
type Props = {
name: string;
theme: string;
};
// export const Greeting: FC<Props> = memo(() => {
export const Greeting: FC<Props> = memo(({ name, theme }) => {
// const { name, theme } = useContext(ThemeContext);
return <h3 className={theme}>Hello, {name}!</h3>;
});
// import { memo, useContext } from 'react';
import { memo } from 'react';
import type { ChangeEventHandler, FC } from 'react';
// import { ThemeContext } from './App';
type Props = {
name: string;
onChange: ChangeEventHandler;
};
// export const NameEditor: FC<Props> = memo(({ onChange }) => {
export const NameEditor: FC<Props> = memo(({ name, onChange }) => {
console.log('NameEditor was rendered at', new Date().toLocaleTimeString());
// const { name } = useContext(ThemeContext);
return <input type="text" value={name} onChange={onChange} />;
});
前掲コードの書き替えを加えたつぎのサンプル002をお試しください。動きはサンプル001と変わりません。けれど、[Switch theme]ボタンをクリックしても、NameEditorコンポーネントは再レンダーされなくなりました。
サンプル002■React + TypeScript: memo 05