useContext
useContext
はコンポーネントでコンテクスト (Context) の読み取りとサブスクライブ(subscribe, 変更の受け取り)を行うための React フックです。
const value = useContext(SomeContext)
リファレンス
useContext(SomeContext)
コンポーネントのトップレベルで useContext
を呼び出して、コンテクストを読み取り、サブスクライブします。
import { useContext } from 'react';
function MyComponent() {
const theme = useContext(ThemeContext);
// ...
引数
SomeContext
: 事前にcreateContext
で作成したコンテクストです。コンテクストとはそれ自体が情報を保持しているわけではなく、コンポーネントで提供 (provide) したり読み取ったりできる「情報の種別」を表すものです。
返り値
useContext
は、呼び出したコンポーネントに対応するコンテクストの値を返します。値は、ツリー内で useContext
を呼び出したコンポーネントの上位かつ最も近い SomeContext.Provider
に渡された value
として決定されます。そのようなプロバイダが存在しない場合は、返り値はそのコンテクストの createContext
に渡した defaultValue
になります。返り値は常にコンテクストの最新の値です。React は、コンテクストに変更があると、それを読み取っているコンポーネントを自動的に再レンダーします。
注意点
- コンポーネントの
useContext()
呼び出しは、同じコンポーネントから返されるプロバイダの影響を受けません。対応する<Context.Provider>
は、useContext()
を呼び出すコンポーネントの上にある必要があります。 - あるコンテクストのプロバイダが異なる
value
を受け取ると、当該プロバイダより下にありそのコンテクストを使用しているすべての子コンポーネントは、React によって自動的に再レンダーされます。前の値と次の値は、Object.is
で比較されます。memo
を使って再レンダーをスキップする場合でも、子コンポーネントがコンテクストから新しい値を受け取ることによる再レンダーは妨げられません。 - ビルドシステムが生成する出力の中にモジュールの重複がある場合(シンボリックリンクで起こり得る場合がある)、コンテクストが壊れる可能性があります。コンテクストを介した値の受け渡しが動作するのは、コンテクストを提供するために使用する
SomeContext
と、読み込むために使用するSomeContext
が、===
による比較で厳密に同じオブジェクトである場合のみです。
使い方
ツリーの深くにデータを渡す
コンポーネントのトップレベルで useContext
を呼び出してコンテクストを読み取り、サブスクライブします。
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext);
// ...
useContext
は渡したコンテクストに対応するコンテクストの値を返します。コンテクストの値を決定するために、React はコンポーネントツリーを探索し、そのコンテクストに対して最も近い上位のコンテクストプロバイダを見つけます。
コンテクストを上記の Button
に渡すには、該当のボタンあるいはその親コンポーネントのいずれかを、対応するコンテクストプロバイダでラップします。
function MyPage() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
);
}
function Form() {
// ... renders buttons inside ...
}
プロバイダと Button
の間にどれだけ多くのコンポーネントが挟まっていても関係ありません。Form
の内部のどこかで Button
が useContext(ThemeContext)
を呼び出すとき、値として "dark"
を受け取ります。
import { createContext, useContext } from 'react'; const ThemeContext = createContext(null); export default function MyApp() { return ( <ThemeContext.Provider value="dark"> <Form /> </ThemeContext.Provider> ) } function Form() { return ( <Panel title="Welcome"> <Button>Sign up</Button> <Button>Log in</Button> </Panel> ); } function Panel({ title, children }) { const theme = useContext(ThemeContext); const className = 'panel-' + theme; return ( <section className={className}> <h1>{title}</h1> {children} </section> ) } function Button({ children }) { const theme = useContext(ThemeContext); const className = 'button-' + theme; return ( <button className={className}> {children} </button> ); }
コンテクスト経由で渡されたデータの更新
多くの場合、時間とともにコンテクストを変化させたいと思うでしょう。コンテクストを更新するには、それを state と組み合わせます。親コンポーネントで state 変数を宣言し、現在の state をコンテクストの値としてプロバイダに渡します。
function MyPage() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Form />
<Button onClick={() => {
setTheme('light');
}}>
Switch to light theme
</Button>
</ThemeContext.Provider>
);
}
これにより、プロバイダの内部にある、どの Button
も現在の theme
の値を受け取るようになります。setTheme
を呼び出してプロバイダに渡す theme
値を更新すると、すべての Button
コンポーネントは新たな値である 'light'
を使って再レンダーされます。
例 1/5: コンテクスト経由で渡された値の更新
この例では、MyApp
コンポーネントが state 変数を保持し、それが ThemeContext
プロバイダに渡されます。“Dark mode” のチェックボックスを選択すると、state が更新されます。プロバイダに渡す値を更新すると、そのコンテクストを使用しているすべてのコンポーネントが再レンダーされます。
import { createContext, useContext, useState } from 'react'; const ThemeContext = createContext(null); export default function MyApp() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={theme}> <Form /> <label> <input type="checkbox" checked={theme === 'dark'} onChange={(e) => { setTheme(e.target.checked ? 'dark' : 'light') }} /> Use dark mode </label> </ThemeContext.Provider> ) } function Form({ children }) { return ( <Panel title="Welcome"> <Button>Sign up</Button> <Button>Log in</Button> </Panel> ); } function Panel({ title, children }) { const theme = useContext(ThemeContext); const className = 'panel-' + theme; return ( <section className={className}> <h1>{title}</h1> {children} </section> ) } function Button({ children }) { const theme = useContext(ThemeContext); const className = 'button-' + theme; return ( <button className={className}> {children} </button> ); }
value="dark"
は "dark"
という文字列を渡しますが、value={theme}
は JavaScript の theme
変数の値を JSX の波括弧で渡していることに注意してください。波括弧を使うことで、文字列以外のコンテクスト値も渡すことができます。
フォールバックとなるデフォルト値の指定
React があるコンテクストに対応するプロバイダを親ツリーで見つけられない場合、useContext()
が返すコンテクストの値は、コンテクストを作成したときに指定したデフォルト値と等しくなります:
const ThemeContext = createContext(null);
デフォルト値は絶対に変更されません。コンテクストを更新したい場合、上記で説明したように、state と組み合わせて使用します。
多くの場合、null
の代わりにデフォルト値として使える、意味のある値があるはずです。例えば:
const ThemeContext = createContext('light');
こうすれば、対応するプロバイダなしにコンポーネントを間違ってレンダーしてしまっても、壊れることはありません。テスト環境でも、テストコードにプロバイダをたくさん設定せずともコンポーネントがうまく動作するようになります。
下記の例では、“Toggle theme” ボタンはあらゆるテーマコンテクストプロバイダの外部にあり、かつテーマコンテクストのデフォルト値が 'light'
であるため、常に light の色調で表示されます。テーマの初期値を 'dark'
に変更してみてください。
import { createContext, useContext, useState } from 'react'; const ThemeContext = createContext('light'); export default function MyApp() { const [theme, setTheme] = useState('light'); return ( <> <ThemeContext.Provider value={theme}> <Form /> </ThemeContext.Provider> <Button onClick={() => { setTheme(theme === 'dark' ? 'light' : 'dark'); }}> Toggle theme </Button> </> ) } function Form({ children }) { return ( <Panel title="Welcome"> <Button>Sign up</Button> <Button>Log in</Button> </Panel> ); } function Panel({ title, children }) { const theme = useContext(ThemeContext); const className = 'panel-' + theme; return ( <section className={className}> <h1>{title}</h1> {children} </section> ) } function Button({ children, onClick }) { const theme = useContext(ThemeContext); const className = 'button-' + theme; return ( <button className={className} onClick={onClick}> {children} </button> ); }
ツリーの一部でコンテクストの値を上書きする
異なる値を持つプロバイダでツリーの一部をラップすることにより、その部分のコンテクストを上書きできます。
<ThemeContext.Provider value="dark">
...
<ThemeContext.Provider value="light">
<Footer />
</ThemeContext.Provider>
...
</ThemeContext.Provider>
プロバイダのネストと上書きは必要なだけ行うことができます。
例 1/2: テーマの上書き
この例では、Footer
の内部にあるボタンは、外部にあるボタン("dark"
)とは違うコンテクスト値("light"
)を受け取ります。
import { createContext, useContext } from 'react'; const ThemeContext = createContext(null); export default function MyApp() { return ( <ThemeContext.Provider value="dark"> <Form /> </ThemeContext.Provider> ) } function Form() { return ( <Panel title="Welcome"> <Button>Sign up</Button> <Button>Log in</Button> <ThemeContext.Provider value="light"> <Footer /> </ThemeContext.Provider> </Panel> ); } function Footer() { return ( <footer> <Button>Settings</Button> </footer> ); } function Panel({ title, children }) { const theme = useContext(ThemeContext); const className = 'panel-' + theme; return ( <section className={className}> {title && <h1>{title}</h1>} {children} </section> ) } function Button({ children }) { const theme = useContext(ThemeContext); const className = 'button-' + theme; return ( <button className={className}> {children} </button> ); }
オブジェクトや関数を渡すときの再レンダーの最適化
コンテクストを介して、オブジェクトや関数を含んだどんな値も渡すことができます。
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
function login(response) {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}
return (
<AuthContext.Provider value={{ currentUser, login }}>
<Page />
</AuthContext.Provider>
);
}
ここでは、コンテクストの値は、2 つのプロパティを持つ JavaScript オブジェクトであり、そのうちの 1 つは関数になります。MyApp
が再レンダーされるたびに(例えば、ページ遷移など)、これは異なる関数の入った異なるオブジェクトを指すため、React はツリーにある useContext(AuthContext)
を呼び出しているすべてのコンポーネントを再レンダーしなければなりません。
小規模なアプリでは、問題になりません。ですが、currentUser
のような内部のデータが変更されていないなら、再レンダーする必要はありません。データが変わっていないという事実を React が最大限に活用できるように、login
関数を useCallback
でラップし、オブジェクトの生成を useMemo
でラップすることができます。これはパフォーマンスの最適化です:
import { useCallback, useMemo } from 'react';
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
const login = useCallback((response) => {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}, []);
const contextValue = useMemo(() => ({
currentUser,
login
}), [currentUser, login]);
return (
<AuthContext.Provider value={contextValue}>
<Page />
</AuthContext.Provider>
);
}
この変更の結果、MyApp
が再レンダーする必要があっても、currentUser
が変更されていない限り、useContext(AuthContext)
を呼び出しているコンポーネントを再レンダーする必要はなくなります。
詳しくは useMemo
と useCallback
を参照してください。
トラブルシューティング
プロバイダに渡した値が自分のコンポーネントから見えない
これが起こる一般的な理由はいくつかあります:
useContext()
を呼び出しているコンポーネントと同じ(または下位の)コンポーネントで<SomeContext.Provider>
をレンダーしている。<SomeContext.Provider>
をuseContext()
を呼び出すコンポーネントの外側かつ上位に移動してください。- コンポーネントを
<SomeContext.Provider>
でラップし忘れているか、ツリー内の思っているのとは違う場所に配置してしまっている。React DevTools を使ってツリー階層が正しいか確認してみてください。 - プロバイダコンポーネントから見た
SomeContext
と、利用側のコンポーネントから見たSomeContext
が、ビルドツールの問題により 2 つの異なるオブジェクトになっている。これは例えば、シンボリックリンクを使用している場合などに発生します。これを確認するために、それらをwindow.SomeContext1
やwindow.SomeContext2
のようなグローバル変数に割り当て、コンソールでwindow.SomeContext1 === window.SomeContext2
が成り立つか確認してみてください。もし同一でないなら、ビルドツールのレベルで、その問題を修正する必要があります。
違うデフォルト値を指定しているのにコンテクストから常に undefined
が返ってくる
ツリーの中に value
のないプロバイダがあるのかもしれません:
// 🚩 Doesn't work: no value prop
<ThemeContext.Provider>
<Button />
</ThemeContext.Provider>
value
を指定し忘れた場合、それは value={undefined}
を渡すのと同じです。
また、誤って props として違う名前を使っているのかもしれません:
// 🚩 Doesn't work: prop should be called "value"
<ThemeContext.Provider theme={theme}>
<Button />
</ThemeContext.Provider>
どちらの場合も、React からの警告がコンソールに表示されるはずです。修正するには、props として value
を使います:
// ✅ Passing the value prop
<ThemeContext.Provider value={theme}>
<Button />
</ThemeContext.Provider>
createContext(defaultValue) で指定するデフォルト値は、ツリーの上側に一致するプロバイダが一切存在しない場合にのみ使用されることに注意してください。親のツリーのどこかに <SomeContext.Provider value={undefined}>
のようなコンポーネントがあれば、useContext(SomeContext)
を呼び出すコンポーネントはコンテクスト値としてその undefined
を受け取ります。