LoginSignup
1
2

React + TypeScript: useContextフックでコンポーネントツリーに渡された情報を受け取る

Last updated at Posted at 2024-05-16

コンテクスト(context)は、コンポーネントツリーに情報を渡す仕組みです。直接の子でなく、深い階層にある子コンポーネントであっても、useContextフックによりコンテクストの値を読み取り、その更新が受け取れます(subscribe)。

本稿はReact公式サイト「useContext」にもとづき、useContextはどう使うのか、およびどのような場合に使うとよいのかを解説します。説明内容と順序は、公式ドキュメントにしたがいました。ただし、解説はわかりやすく改め、またコード例とサンプル(StackBlitz)はTypeScriptを加えたうえで修正した部分が少なくありません。

構文

const value = useContext(SomeContext)

フックuseContextは、コンポーネントのトップレベルで呼び出してください。フックが読み込んだコンテクストから、更新された値を受け取ります。

import { useContext } from 'react';
import { ThemeContext } from './App';

export const MyComponent: FC = () => {
	const theme = useContext(ThemeContext);

}

useContextの構文はつぎのとおりです。

useContext(SomeContext) 

引数

  • SomeContext: あらかじめcreateContextでつくったコンテクスト。
    • 異なるモジュールの子コンポーネントから用いるには、コンテクストをexportしてください。
    • コンテクストそのものは情報をもちません。コンポーネントから提供(provide)し、読み取れる「情報の種別」です。

戻り値

useContextフックを呼び出したコンポーネントに応じたコンテクストの値が返されます。

  • コンポーネントツリーを上層に向けて探し、もっとも近いSomeContext.Providerに渡されたvalueの値です。
  • ツリーの上層にプロバイダが見つからない場合は、コンテクストをつくったcreateContextの引数に渡したデフォルト値(defaultValue)となります。

戻り値は、つねにコンテクストの最新値です。Reactは、コンテクストが更新されると、読み取っているコンポーネントを自動的に再レンダーします。

注意

  • useContextの呼び出しは、そのコンポーネント自身の戻り値(JSX)に加えられたプロバイダの値を対象としません。useContextを呼び出したコンポーネントから、ツリーの上層に向かって<Context.Provider>が探されるからです。
  • コンテクストプロバイダの受け取るvalueが変わると、コンポーネントツリー内でそのコンテクストを用いるすべての子コンポーネントはReactにより自動的に再レンダーされます。
  • ビルドシステムの生成する出力の中でモジュールが重複する場合(シンボリックリンクで起こり得ます)、コンテクストは壊れるかもしれないことにご注意ください。
    • コンテクストによる値の受け渡しが動作するには、提供するために用いるSomeContextと読み込む側のSomeContextは、===による比較で厳密に同じオブジェクトでなければなりません。

使い方

コンポーネントツリーの深い下層にある子にデータを渡す

useContextフックは、コンポーネントのトップレベルで呼び出してください。読み込んだコンテクストの最新の値が受け取れます(subscribe)。

src/Button.tsx
import { useContext } from 'react';
import { ThemeContext } from './App';

export const Button: FC<Props> = ({ children }) => {
	const theme = useContext(ThemeContext);
	const className = 'button-' + theme;
	return <button className={className}>{children}</button>;
};

useContextが返すのは、引数で渡したコンテクストに応じた最新の値です。Reactがコンポーネントツリーを上層に向けて探し、もっとも近いコンテクストプロバイダの値を得ます。したがって、コンテクストプロバイダは、値の使われる(useContext呼び出しする)子コンポーネントが含まれるツリー上層のいずれかの親をラップしなければならないのです。

src/App.tsx
import { createContext } from 'react';
import { Form } from './Form';

const defaultTheme = 'dark';
export const ThemeContext = createContext(defaultTheme);
function App() {
	return (
		<ThemeContext.Provider value={defaultTheme}>
			<Form />
		</ThemeContext.Provider>
	);
}

上記コード例では、コンポーネントButtonApp直下の子ではありません。けれど、プロパティ(props)とは異なり、コンテクストは子のツリー上であればコンポーネントが間に何階層加わっても構わないのです。

src/Form.tsx
export const Form: FC = () => {
	return (
		<Panel title="Welcome">
			<Button>Sign up</Button>
			<Button>Log in</Button>
		</Panel>
	);
};
src/Panel.tsx
import { useContext } from 'react';
import { ThemeContext } from './App';

export const Panel: FC<Props> = ({ title, children }) => {
	const theme = useContext(ThemeContext);
	const className = 'panel-' + theme;
	return (
		<section className={className}>
			<h1>{title}</h1>
			{children}
		</section>
	);
};

つぎのサンプル001では、コンポーネントはApp > Form > Panel > Buttonというツリーになっています。それでも、AppのモジュールからcreateContextでつくったコンテクスト(ThemeContext)の値は、Buttonコンポーネント内でuseContextを呼び出して読み取れるのです。themeの値はdefaultThemeですので、'dark'に対応したCSS(className)が適用されました(なお、Panelthemeの値に応じて要素のスタイルが変わります)。

サンプル001■React + TypeScript: useContext 01

もっとも、サンプル001では、themeの値はdefaultTheme('dark')の決め打ちです。このままでは、切り替えはできせん。

[注記] useContextは、つねに呼び出されたコンポーネントツリーの上層に向けてプロバイダを探します。したがって、コンポーネント自身の戻り値(JSX)に加えられたプロバイダは対象となりません。フックは必ずコンポーネントツリーの子から親に対して呼び出してください。

コンテクストが渡すデータを更新する

コンテクストから渡すデータが決め打ちでは使えないでしょう。更新するには、値を親コンポーネントの状態変数にしてください。コンテクストのプロバイダにvalue値として渡すのはその状態変数です。

親コンポーネントAppに、チェックボックス(<input type="checkbox")を加えました。クリック(onChange)で切り替えるのが状態変数themeの値です。ツリー内の子コンポーネントは、すでにコンテキスト(ThemeContext)のvalue値が反映されるようになっています。

src/App.tsx
// import { createContext } from 'react';
import { createContext, useState } from 'react';

function App() {
	const [theme, setTheme] = useState(defaultTheme);
	return (
		// <ThemeContext.Provider value={defaultTheme}>
		<ThemeContext.Provider value={theme}>

			<label>
				<input
					type="checkbox"
					checked={theme === defaultTheme}
					onChange={({ target: { checked } }) => {
						setTheme(checked ? defaultTheme : 'light');
					}}
				/>
				Use dark mode
			</label>
		</ThemeContext.Provider>
	);
}

つぎのサンプル002で、チェックボックスによりコンポーネント(ButtonPanel)のカラーが切り替わることをお確かめください。プロバイダに渡すvalue値が変わると、コンテクストを使っているすべての子コンポーネントは新たな値で再レンダーされるのです。

サンプル002■React + TypeScript: useContext 02

React公式サイトの「「Updating data passed via context」」には、簡単な説明とともにあと4つのサンプルが公開されています。これらは「React + TypeScript: コンテクストが渡すデータを更新するコード例」に記事を改めて解説しましたので、ご参照ください。

フォールバックされるコンテクストのデフォルト値を定める

useContext()が返すコンテクストの値は、プロバイダに渡されたvalueで決まります。したがって、createContextの引数をnullにしても直ちには問題となりません。

src/App.tsx
const ThemeContext = createContext(null);

function App() {
	const [theme, setTheme] = useState('dark');
	return (
		<ThemeContext.Provider value={theme}>

		</ThemeContext.Provider>
	);
}

ただし、ツリーを親コンポーネントに遡ってプロバイダが見つからない場合は、useContext()の戻り値はcreateContextの引数に渡したデフォルト値です。

つぎのButtonコンポーネントのように誤ってプロバイダ(<ThemeContext.Provider>)のラップから外してしまうと、useContextの戻り値(theme)はデフォルト値nullとなってしまうでしょう。

src/App.tsx
function App() {

	return (
		<>
			<ThemeContext.Provider value={theme}>

			</ThemeContext.Provider>
			<Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
				Toggle theme
			</Button>

		</>
	);
}
src/Button.tsx
export const Button: FC<Props> = ({ children, onClick }) => {
	const theme = useContext(ThemeContext);
	const className = 'button-' + theme;
	return (
		<button className={className} onClick={onClick}>
			{children}
		</button>
	);
};

createContextにデフォルト値を与えることで、問題が広がるのを抑えられます。コンテクストのデフォルト値はあとから変えられません。コンテクストの値は、前述のとおり状態変数と組み合わせて更新してください(サンプル003)。

src/App.tsx
const defaultTheme = 'dark';
export const ThemeContext = createContext(defaultTheme);
function App() {
	const [theme, setTheme] = useState(defaultTheme);
	return (
		<ThemeContext.Provider value={theme}>

			<Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
				Toggle theme
			</Button>
		</ThemeContext.Provider>
	);
}

サンプル003■React + TypeScript: useContext 03

なお、TypeScriptを使っていれば、createContextのデフォルト値にnullを渡すと、プロバイダから与えるvalueの値に問題があると警告されるでしょう。初期値をあえてnullに定めたいときは、createContextに型づけしてください。

export const ThemeContext = createContext<string | null>(null);

コンポーネントツリーの一部のコンテクストを上書きする

コンポーネントツリーの一部を上層とはvalue値が異なるコンテクストで包めば、下層の値は上書きできます。プロバイダの入れ子による値の上書きは、いくつ行っても構いません。

コンテクストプロバイダで包まれた子コンポーネントの一部を別のプロバイダでラップする

つぎのアプリケーションは、コンテクストプロバイダに与えた値(<ThemeContext.Provider value={defaultTheme}>)に応じて、要素のカラー(CSS)を切り替えます(値は'dark'です)。

src/App.tsx
const defaultTheme = 'dark';
export const ThemeContext = createContext(defaultTheme);
function App() {
	return (
		<ThemeContext.Provider value={defaultTheme}>
			<Form />
		</ThemeContext.Provider>
	);
}

親コンポーネントでコンテクストプロバイダに包まれたFormは、改めて戻り値のJSXの一部をプロバイダ(<ThemeContext.Provider value="light">)でラップしました。加えたのは、コンポーネントFooterで、与えたvalueは異なる値("light")です。

src/Form.tsx
import { ThemeContext } from './App';

export const Form: FC = () => {
	return (
		<Panel title="Welcome">
			<Button>Sign up</Button>
			<Button>Log in</Button>
			<ThemeContext.Provider value="light">
				<Footer />
			</ThemeContext.Provider>
		</Panel>
	);
};

すると、コンポーネントFooterの中の[Settings]ボタン(Button)は、ツリー上層のもっとも近いプロバイダからvalue値("light")を得て、他のふたつのボタンとは違うカラー(CSS)が適用されます。サンプル004でお確かめください。ただし、ボタンにインタラクションは与えられていません。

src/
export const Footer: FC = () => {
	return (
		<footer>
			<Button>Settings</Button>
		</footer>
	);
};

サンプル004■React + TypeScript: useContext 04

入れ子にしたコンテクストプロバイダで親の値から子に渡す値を定める

コンテクストプロバイダを入れ子にした場合、親から受け取った値に応じて子に渡す値が定められます。

つぎのモジュールsrc/App.tsxで入れ子になっているのは、コンポーネントSectionです。

src/App.tsx
const Headings = (count: number, title: string) =>
	Array.from(new Array(count), (_) => <Heading>{title}</Heading>);
function App() {
	return (
		<Section>
			<Heading>Title</Heading>
			<Section>
				{Headings(3, 'Heading')}
				<Section>
					{Headings(3, 'Sub-heading')}
					<Section>{Headings(3, 'Sub-sub-heading')}</Section>
				</Section>
			</Section>
		</Section>
	);
}

そして、Sectionコンポーネントが返すJSXは子(children)をコンテクストプロバイダ(<LevelContext.Provider>)で包んでいます。つまり、コンポーネントとともに、コンテクストプロバイダも入れ子になるということです。valueとして渡す値は、親コンテクストの値(level)に1加算しています。

src/Section.tsx
export const Section: FC<Props> = ({ children }) => {
	const level = useContext(LevelContext);
	return (
		<section className="section">
			<LevelContext.Provider value={level + 1}>
				{children}
			</LevelContext.Provider>
		</section>
	);
};

コンテクストの初期値(0)に1ずつ加算して、入れ子のコンテクストプロバイダにvalueとして渡るということです。

src/LevelContext.ts
import { createContext } from 'react';

export const LevelContext = createContext(0);

コンポーネントSectionに差し込まれたHeadingは、useContextの値(level)に応じて、<h1><h6>にテキスト(children)を加えて返します。

src/Heading.tsx
export const Heading: FC<Props> = ({ children }) => {
	const level = useContext(LevelContext);
	switch (level) {
		case 0:
			throw Error('Heading must be inside a Section!');
		case 1:
			return <h1>{children}</h1>;
		case 2:
			return <h2>{children}</h2>;
		case 3:
			return <h3>{children}</h3>;
		case 4:
			return <h4>{children}</h4>;
		case 5:
			return <h5>{children}</h5>;
		case 6:
			return <h6>{children}</h6>;
		default:
			throw Error('Unknown level: ' + level);
	}
};

<section>で入れ子にされた見出し(<Heading>)は、<h1>から<h6>まで順に要素を切り替えて表示されるでしょう(サンプル005)。このコード例について段階を踏んだ詳しい解説は、「React + TypeScript: コンテクストでデータを深い階層に渡す」をお読みください。

サンプル005■React + TypeScript: useContext 05

オブジェクトや関数を渡すとき再レンダーを最適化する

コンテクストを用いて渡せるのは、オブジェクトや関数も含めた任意の値です。つぎのアプリケーションは、コンテクストプロバイダにふたつのプロパティが収められたオブジェクトを与えています。プロパティのひとつ(login)は関数です。

const AuthContext = createContext(defaultContext);
function App() {
	const [currentUser, setCurrentUser] = useState('');
	const login = (response: Response) => {
		storeCredentials(response.credentials);
		setCurrentUser(response.user);
	};
	return (
		<AuthContext.Provider value={{ currentUser, login }}>
			<Page />
		</AuthContext.Provider>
	);
}

コンポーネント内に定められた関数は、再レンダーのたびに定義し直されることに気をつけなければなりません。再定義された関数は、中身が変わらなくとも、参照にもとづいて別と評価されます。すると、コンテクストの中の関数は定義が変わったとみなされ、useContext(AuthContext)を用いる子コンポーネントはすべて再レンダーされるのです。

アプリケーションがまだ小さければ、気にしなくても構いません。とはいえ、コンテクストの値としたオブジェクトのもうひとつのプロパティ(currentUser)も変わっていないなら、無駄に再レンダーしなくてもよいでしょう。

関数loginuseCallbackで包めば、Reactは関数定義が実質的に変わったか確かめられます。さらに、コンテクストプロバイダのvalueに渡すオブジェクト(contextValue)をメモ化してしまえるのがuseMemoです。このようにして、パフォーマンスが最適化できます。

function App() {

	// const login = (response: Response) => {
	const login = useCallback((response: Response) => {
		storeCredentials(response.credentials);
		setCurrentUser(response.user);
		// };
	}, []);
	const contextValue = useMemo(
		() => ({
			currentUser,
			login,
		}),
		[currentUser, login]
	);
	return (
		// <AuthContext.Provider value={{ currentUser, login }}>
		<AuthContext.Provider value={contextValue}>

		</AuthContext.Provider>
	);
}

loginに関数を返すuseCallbackに依存はありません([])。contextValueにオブジェクトを収めるuseMemoの依存値は、currentUserloginです。したがって、コンポーネントAppは再レンダーされても、useContext(AuthContext)を呼び出している子コンポーネントはcurrentUserの値が変わらないかぎり再レンダーされません。

詳しくは、「React + TypeScript: useCallbackフックの使い方と使いどころ」と「React + TypeScript: useMemoフックの使い方と使いどころ」をお読みください。

トラブルへの対応

コンテクストプロバイダに与えられているvalue値がコンポーネントから受け取れない

この問題でよくある理由はつぎの3つです。

  1. useContextを呼び出すコンポーネントは、コンテクストプロバイダ(<SomeContext.Provider>)に包まれた子でなければなりません。
    • 自分のコンポーネントが返すJSXにレンダーしたコンテクストプロバイダのvalue値は得られません。
    • コンテクストプロバイダ(<SomeContext.Provider>)は、コンポーネントの外側、ツリーの上層に移してください。
  2. コンポーネントがコンテクストプロバイダ(<SomeContext.Provider>)のラップから外れていることが考えられます。
  3. コンテクスト(SomeContext)の参照が、プロバイダコンポーネントと利用側コンポーネントとの間で食い違っているかもしれません。
    • シンボリックリンクを使っていたり、ビルドツールの問題により、ふたつのブジェクトは異なって認識される場合があります。
    • ふたつの参照をwindow.SomeContext1window.SomeContext2といったグローバル変数に割り当て、コンソールでwindow.SomeContext1 === window.SomeContext2が成り立つか確かめてください。
      • 等しくない場合は、ビルドツールのレベルで解決しなければなりません。

コンテクストのデフォルト値を変えても得られる値がつねにundefinedになる

createContextの引数に与えたデフォルト値は、コンポーネントツリーの上層にコンテクストプロバイダがない場合に返されます。したがって、つぎのような点をお確かめください。

  • コンポーネントツリーの上層にコンテクストプロバイダはあって、valueが与えられていないのかもしれません。
    • valueがなければ、value={undefined}と同じです。
// 🚩 NG: コンテクストプロバイダにvalueが与えられていない
<ThemeContext.Provider>
	<Button />
</ThemeContext.Provider>
  • 子コンポーネントにpropsを渡すときと勘違いして、value以外の名前を使ってしまうことがあります。
// 🚩 NG: コンテクストの値を渡すのはvalueです
<ThemeContext.Provider theme={theme}>
	<Button />
</ThemeContext.Provider>

ただ、どちらの場合もReactから警告が示されるでしょう。コンテクストプロバイダに値はvalueで渡してください。

// ✅ OK: コンテクストの値はvalueで渡す
<ThemeContext.Provider value={theme}>
	<Button />
</ThemeContext.Provider>

前述のとおり、createContextの引数に与えたデフォルト値が返されるのは、コンポーネントツリーの上層にコンテクストプロバイダがない場合です。ツリー上層にコンテクストプロバイダ<SomeContext.Provider value={someValue}>はあり、someValueundefinedなら、コンポーネントからuseContext(SomeContext)を呼び出して受け取るコンテクスト値はundefinedになるでしょう。

1
2
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
1
2