Menu

React useContextの使い方:propsを使わずにデータを共有する

useContextは親コンポーネントが提供する値を読むので、深くネストしたコンポーネントは、途中のすべてのコンポーネントがpropとして渡さなくてもその値を受け取れます。createContext、プロバイダー、子からのコンテキストの更新、そしてコンテキストが再レンダリングにどう影響するかを学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

useContext は親コンポーネントが提供する値を読むので、ツリーの深いところにあるコンポーネントは、途中のすべてのコンポーネントがpropとして渡さなくてもその値を受け取れます。createContext でコンテキストを作り、ツリーの一部を value 付きのプロバイダーで包み、その値が必要な場所で useContext(TheContext) を呼びます。

Toolbar はテーマに一切触れていないのに、チェックボックスを入れると両方のボタンが変わります。これこそがコンテキストの要点です。途中のコンポーネントは、使わないpropを運ばずに済みます。コンテキストが解決するのは、このpropsのバケツリレーの問題です。

3つのステップ

// 1. Create it once, outside any component, usually in its own file
export const ThemeContext = createContext('light');

// 2. Provide a value to a part of the tree
<ThemeContext value={theme}>
    <Page />
</ThemeContext>

// 3. Read it in any component inside that part
const theme = useContext(ThemeContext);

React 19では、コンテキストオブジェクトそのものがプロバイダーです。古いコードでは <ThemeContext.Provider value={theme}> と書き、これも動くので、両方を目にするでしょう。

useContext は、そのコンテキストの最も近いプロバイダーをツリーの上へ探します。コンポーネントは一番近いものを読むので、プロバイダーをネストすれば、ある部分だけ値を上書きできます。

デフォルト値

createContext の引数は、コンポーネントより上にプロバイダーがないときに useContext が返す値です。stateが変わってもこれは変わりません。代わりに使われる値です。

1つ目の Greeting は上にプロバイダーがないので、デフォルトの 'en' を受け取ります。2つ目は 'es' を読み、3つ目はより近い 'ja' を読みます。外側の value="es" を value="ja" に変えると、2行目だけが変わります。

子からコンテキストを更新する

コンテキストは値を下へ渡すだけです。深い子にそれを変えさせたいなら、プロバイダーのある場所で値をstateに持ち、セッターをその隣に並べてコンテキストに入れます。

LoginButton が setUser を呼ぶと、App のstateが変わり、プロバイダーが新しい値を受け取り、Header も更新されます。どちらのコンポーネントもpropは受け取っていません。

値が変わるとすべての利用者が再レンダリングされる

プロバイダーの value が変わると、Reactはそれに対して useContext を呼ぶすべてのコンポーネントを、たとえ memo の中にあってpropsが同じでもレンダリングします。Reactは古い値と新しい値を Object.is で比べます。そのため value={{ user, setUser }} は App がレンダリングされるたびに新しいオブジェクトになり、user が変わっていなくても、すべての利用者が再レンダリングされます。

最初のレンダリングで、どちらの読み手も1回ログを出します。ボタンをクリックすると、PlainReader だけがもう一度ログを出します。そのコンテキストが新しいオブジェクトを受け取ったからです。useMemo は name が変わるまで MemoContext に同じオブジェクトを渡すので、MemoReader はそのままです。これが問題になるのはレンダリングが遅くなってからで、小さなアプリならレンダリングごとに新しいオブジェクトでも問題ありません。

コンテキストを分ける

頻繁に変わる値と決して変わらない値を、同じコンテキストに入れるべきではありません。セッターがstateと一緒にあると、setUser を呼ぶだけのコンポーネントも、user が変わるたびにレンダリングされます。2つのコンテキストに分ければ、各利用者は自分が読むものだけを購読します。

const UserContext = createContext(null);
const SetUserContext = createContext(() => {});

function UserProvider({ children }) {
    const [user, setUser] = useState(null);
    return (
        <SetUserContext value={setUser}>
            <UserContext value={user}>{children}</UserContext>
        </SetUserContext>
    );
}

useState の setUser はずっと同じ同一性を保つので、SetUserContext だけを読むコンポーネントがログインのせいでレンダリングされることはありません。同じ考え方は無関係なデータにも当てはまります。テーマとショッピングカートは、1つの大きな AppContext ではなく、別々のコンテキストに入れるべきです。

よくある仕上げとして、useContext(UserContext) を呼び、プロバイダーがないときにわかりやすいエラーを投げる useUser() のようなカスタムフックを用意します。そうすれば呼び出す側がコンテキストオブジェクトを直接インポートすることはなくなります。

コンテキスト、props、状態管理ライブラリ

  • propsが基本です。2、3階層下へ値を渡すのは明快で、追うのも簡単です。
  • コンテキストは、多くのコンポーネントがさまざまな深さで必要とし、めったに変わらない値に向いています。テーマ、言語、ログイン中のユーザー、機能フラグのオブジェクトなどです。
  • 状態管理ライブラリ(Redux Toolkit、Zustand、Jotaiなど)は、頻繁に更新される大きなstateで、コンポーネントに一部のスライスだけを購読させ、それ以外が原因のレンダリングを省きたい場合に向いています。コンテキストにはそうしたセレクターがなく、利用者は値のどんな変化でもレンダリングされます。

コンテキストやライブラリに手を伸ばす前に、コンポーネントを children として渡せばバケツリレーがなくなるかどうかを確かめてください。新しいAPIをまったく使わずに解決することがよくあります。

よくある質問

ReactのuseContextは何をするものですか?

コンテキストの現在の値を返します。値はツリー内でそのコンポーネントより上にある、最も近いプロバイダーから取られます。そのプロバイダーの値が変わると、それを読んでいるすべてのコンポーネントが再レンダリングされます。

createContextのデフォルト値は何のためにありますか?

コンポーネントより上にプロバイダーがないときに useContext が返す値です。テストや単独で描画されるコンポーネントに便利で、決して変わりません。

React 19でもContext.Providerは必要ですか?

いいえ。React 19では、コンテキストそのものをプロバイダーとして描画できます:<ThemeContext value="dark">。<ThemeContext.Provider value="dark"> も引き続き動くので、古いコードもそのまま動きます。

子コンポーネントからコンテキストの値を更新するには?

プロバイダーを描画するコンポーネントで値をstateに持ち、セッターを値と並べてコンテキストに入れます:value={{ theme, setTheme }}。子は useContext で setTheme を読み、それを呼びます。

ReactのコンテキストはReduxの代わりになりますか?

コンテキストは値をツリーの下へ渡すもので、それ自体がstateを管理するわけではありません。useState や useReducer と組み合わせれば、多くのアプリで十分です。状態管理ライブラリは、一部のスライスだけを購読して無関係な更新でコンポーネントを再レンダリングしない仕組みや、開発ツール、ミドルウェアなど、コンテキストにないものを加えます。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める