コンテクスト(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により自動的に再レンダーされます。-
value
が変わったかどうかの比較はObject.is
です。 - 子コンポーネントを
memo
で包んでも、コンテクストは引数に受け取るprops
ではないので、再レンダーは省かれません(「React + TypeScript: memoで包んだコンポーネントでもコンテクストの更新により再レンダーされる」参照)。
-
- ビルドシステムの生成する出力の中でモジュールが重複する場合(シンボリックリンクで起こり得ます)、コンテクストは壊れるかもしれないことにご注意ください。
- コンテクストによる値の受け渡しが動作するには、提供するために用いる
SomeContext
と読み込む側のSomeContext
は、===
による比較で厳密に同じオブジェクトでなければなりません。
- コンテクストによる値の受け渡しが動作するには、提供するために用いる
使い方
コンポーネントツリーの深い下層にある子にデータを渡す
useContext
フックは、コンポーネントのトップレベルで呼び出してください。読み込んだコンテクストの最新の値が受け取れます(subscribe)。
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
呼び出しする)子コンポーネントが含まれるツリー上層のいずれかの親をラップしなければならないのです。
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>
);
}
上記コード例では、コンポーネントButton
はApp
直下の子ではありません。けれど、プロパティ(props
)とは異なり、コンテクストは子のツリー上であればコンポーネントが間に何階層加わっても構わないのです。
export const Form: FC = () => {
return (
<Panel title="Welcome">
<Button>Sign up</Button>
<Button>Log in</Button>
</Panel>
);
};
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
)が適用されました(なお、Panel
もtheme
の値に応じて要素のスタイルが変わります)。
サンプル001■React + TypeScript: useContext 01
もっとも、サンプル001では、theme
の値はdefaultTheme
('dark'
)の決め打ちです。このままでは、切り替えはできせん。
[注記] useContext
は、つねに呼び出されたコンポーネントツリーの上層に向けてプロバイダを探します。したがって、コンポーネント自身の戻り値(JSX)に加えられたプロバイダは対象となりません。フックは必ずコンポーネントツリーの子から親に対して呼び出してください。
コンテクストが渡すデータを更新する
コンテクストから渡すデータが決め打ちでは使えないでしょう。更新するには、値を親コンポーネントの状態変数にしてください。コンテクストのプロバイダにvalue
値として渡すのはその状態変数です。
親コンポーネントApp
に、チェックボックス(<input type="checkbox"
)を加えました。クリック(onChange
)で切り替えるのが状態変数theme
の値です。ツリー内の子コンポーネントは、すでにコンテキスト(ThemeContext
)のvalue
値が反映されるようになっています。
// 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で、チェックボックスによりコンポーネント(Button
とPanel
)のカラーが切り替わることをお確かめください。プロバイダに渡すvalue
値が変わると、コンテクストを使っているすべての子コンポーネントは新たな値で再レンダーされるのです。
サンプル002■React + TypeScript: useContext 02
React公式サイトの「「Updating data passed via context」」には、簡単な説明とともにあと4つのサンプルが公開されています。これらは「React + TypeScript: コンテクストが渡すデータを更新するコード例」に記事を改めて解説しましたので、ご参照ください。
フォールバックされるコンテクストのデフォルト値を定める
useContext()
が返すコンテクストの値は、プロバイダに渡されたvalue
で決まります。したがって、createContext
の引数をnull
にしても直ちには問題となりません。
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
となってしまうでしょう。
function App() {
return (
<>
<ThemeContext.Provider value={theme}>
</ThemeContext.Provider>
<Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle theme
</Button>
</>
);
}
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)。
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'
です)。
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"
)です。
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でお確かめください。ただし、ボタンにインタラクションは与えられていません。
export const Footer: FC = () => {
return (
<footer>
<Button>Settings</Button>
</footer>
);
};
サンプル004■React + TypeScript: useContext 04
入れ子にしたコンテクストプロバイダで親の値から子に渡す値を定める
コンテクストプロバイダを入れ子にした場合、親から受け取った値に応じて子に渡す値が定められます。
つぎのモジュールsrc/App.tsx
で入れ子になっているのは、コンポーネントSection
です。
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加算しています。
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
として渡るということです。
import { createContext } from 'react';
export const LevelContext = createContext(0);
コンポーネントSection
に差し込まれたHeading
は、useContext
の値(level
)に応じて、<h1>
〜<h6>
にテキスト(children
)を加えて返します。
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
)も変わっていないなら、無駄に再レンダーしなくてもよいでしょう。
関数login
をuseCallback
で包めば、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
の依存値は、currentUser
とlogin
です。したがって、コンポーネントApp
は再レンダーされても、useContext
(AuthContext
)を呼び出している子コンポーネントはcurrentUser
の値が変わらないかぎり再レンダーされません。
詳しくは、「React + TypeScript: useCallbackフックの使い方と使いどころ」と「React + TypeScript: useMemoフックの使い方と使いどころ」をお読みください。
トラブルへの対応
コンテクストプロバイダに与えられているvalue
値がコンポーネントから受け取れない
この問題でよくある理由はつぎの3つです。
-
useContext
を呼び出すコンポーネントは、コンテクストプロバイダ(<SomeContext.Provider>
)に包まれた子でなければなりません。- 自分のコンポーネントが返すJSXにレンダーしたコンテクストプロバイダの
value
値は得られません。 - コンテクストプロバイダ(
<SomeContext.Provider>
)は、コンポーネントの外側、ツリーの上層に移してください。
- 自分のコンポーネントが返すJSXにレンダーしたコンテクストプロバイダの
- コンポーネントがコンテクストプロバイダ(
<SomeContext.Provider>
)のラップから外れていることが考えられます。- React Developer Toolsを使ってツリー階層が正しいか確かめましょう。
- コンテクスト(
SomeContext
)の参照が、プロバイダコンポーネントと利用側コンポーネントとの間で食い違っているかもしれません。- シンボリックリンクを使っていたり、ビルドツールの問題により、ふたつのブジェクトは異なって認識される場合があります。
- ふたつの参照を
window.SomeContext1
やwindow.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}>
はあり、someValue
がundefined
なら、コンポーネントからuseContext(SomeContext)
を呼び出して受け取るコンテクスト値はundefined
になるでしょう。