LoginSignup
0
1

React + TypeScript: memoで包んだコンポーネントでもコンテクストの更新により再レンダーされる

Posted at

コンポーネントをmemoで包むと、親から渡されたプロパティ(props)が直前と変わらないかぎり再レンダーされません。ただし、コンテクストを使っていると(useContext)、propsは前と同じでも、コンテクストが更新されれば再レンダーされます。簡単なコード例で、その動きと対応方法を探ってみましょう。

ふたつの子コンポーネントでコンテクストを使う

つぎのモジュールsrc/App.tsxがつくるコンテクスト(createContext)は、ふたつの状態変数(nametheme)をvalueとして子のコンポーネントツリーに与えています。子コンポーネントは、NameEditorGreetingのふたつです。あとに示すように、どちらもmemoでラップしてあります。なお、メモ化した子コンポーネントにプロパティとして渡す関数(handleChange)は、useCallbackに包みましょう。状態変数themeの値は[Switch theme]ボタン(<button>)で切り替わります。

src/App.tsx
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)のはふたつの値namethemeです。nameは表示するテキスト、themeはカラー設定のクラス(className)を変更します。

src/Greeting.tsx
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">)のテキストに更新します。

src/NameEditor.tsx
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)で包みましょう。

src/App.tsx
/* 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として与えるのです。

src/Container.tsx
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であれば、コンポーネントのメモ化が働きます。受け取った値が変わらないかぎり、再レンダーされません。

src/Greeting.tsx
// 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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1