コンポーネントを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