1. ホーム
  2. React

【React】useContext と Context API の使い方|propsのバケツリレーを解消して値を共有する

Share

React でアプリを作っていると、「アプリ全体で使いたいテーマの色」や「ログイン中のユーザー情報」など、あちこちのコンポーネントで共有したい値が出てきます。これを props だけで渡そうとすると、途中のコンポーネントが自分では使わない値をひたすら受け渡す、いわゆるバケツリレー(prop drilling)が発生します。そこで役立つのが Context APIuseContext フックです。この記事では、Context を作って値を渡し、離れた子コンポーネントで受け取るまでの流れを、テーマ切り替えやユーザー情報の共有といった具体例で解説します。

props のバケツリレー(prop drilling)とは

React では、親から子へデータを渡す基本の手段は props です。ところが、深くネストしたコンポーネントに値を届けたいとき、間にあるコンポーネントがその値を使わないのに props として中継しなければならないことがあります。これが prop drilling です。次のコードでは、App が持つ user を、LayoutHeader を経由して UserBadge まで手渡ししています。

prop-drilling.tsx
// user を使うのは一番奥の UserBadge だけなのに、
// 途中の Layout と Header も props を中継している
function App() {
  const user = { name: 'たくみ' };
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Header user={user} />;   // 自分では使わないのに受け渡す
}

function Header({ user }) {
  return <UserBadge user={user} />; // ここも中継するだけ
}

function UserBadge({ user }) {
  return <span>こんにちは、{user.name} さん</span>;
}

階層が浅いうちは問題になりませんが、中継するコンポーネントが増えるほどコードが読みにくくなり、props の追加や変更も面倒になります。「離れた場所どうしで同じ値を共有したい」というときに、途中を飛ばして値を届ける仕組みが Context API です。

Context を作る・渡す・受け取るの3ステップ

Context を使う流れは、大きく分けて次の3ステップです。まず createContext で Context を作り、次に <Context.Provider> で値を渡し、最後に子コンポーネントで useContext を使って値を受け取ります。それぞれの登場人物の役割を先に整理しておきましょう。

API・要素役割
createContext(初期値)共有用の Context オブジェクトを作る。引数はデフォルト値
<Context.Provider value={...}>配下のコンポーネントに value の値を配る
useContext(Context)もっとも近い Provider の value を読み取るフック

テーマ(ダーク/ライト)を共有する例

実際に、アプリ全体でテーマ(ダーク/ライト)を共有する例で3ステップを見ていきます。まずは createContext で Context を作ります。引数に渡すのはデフォルト値で、後述するように Provider の外で使われたときに返る値になります。

ThemeContext.tsx
import { createContext } from 'react';

// 'light' がデフォルト値。型も 'light' | 'dark' に絞っておく
export const ThemeContext = createContext<'light' | 'dark'>('light');

次に、値を配りたい範囲を <ThemeContext.Provider> で囲み、value 属性に共有したい値を渡します。Provider で囲んだ内側であれば、どれだけ深い階層のコンポーネントでもこの値を受け取れます。

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

function App() {
  return (
    // この中のどのコンポーネントも theme を 'dark' として受け取れる
    <ThemeContext.Provider value="dark">
      <Layout />
    </ThemeContext.Provider>
  );
}

// 途中の Layout は user のときのような中継が不要になる
function Layout() {
  return <Toolbar />;
}

最後に、値を使いたいコンポーネントで useContext を呼びます。引数に受け取りたい Context オブジェクトを渡すと、もっとも近い Provider の value が返ってきます。ToolbarApp から props を一切受け取っていませんが、Context 経由で theme を読み取れています。

Toolbar.tsx
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function Toolbar() {
  // 近くの Provider の value(ここでは 'dark')が入る
  const theme = useContext(ThemeContext);

  return (
    <div className={theme === 'dark' ? 'toolbar-dark' : 'toolbar-light'}>
      現在のテーマ: {theme}
    </div>
  );
}

これで、間の Layout を経由せずに App から Toolbar へ値が届きました。props のバケツリレーが消え、値を「使う側」と「配る側」だけがコードに現れるようになったのがポイントです。

デフォルト値と、Provider の外で使ったときの挙動

createContext の引数に渡したデフォルト値は、いつ使われるのか気になるところです。答えは、対応する Provider が上位に1つも存在しないまま useContext が呼ばれたときです。Provider で囲まれていれば、その value が優先され、デフォルト値は使われません。

default-value.tsx
const ThemeContext = createContext<'light' | 'dark'>('light');

function Standalone() {
  // このコンポーネントを Provider で囲まずに描画すると…
  const theme = useContext(ThemeContext);
  return <p>{theme}</p>; // デフォルト値の 'light' が表示される
}

この挙動は、コンポーネント単体でのテストや、Provider を用意し忘れたときのフォールバックとして役立ちます。ただし、デフォルト値に頼りすぎると「Provider の付け忘れ」に気づきにくくなります。本来 Provider が必須のケースでは、デフォルト値を null や意味のない値にしておき、useContext を包んだ独自フックの中で「Provider がないときはエラーを投げる」ようにしておくと、付け忘れをすぐ検知できます。

値と更新関数を一緒に渡す(ユーザー情報の共有)

共有したいのは「読み取るだけの値」とは限りません。ログインユーザー情報のように、値そのものと、それを更新する関数の両方を共有したいことがよくあります。その場合は、Provider の内側で useState を持ち、値と更新関数をまとめてオブジェクトにして value に渡します。

AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

type User = { name: string } | null;
// 値(user)と更新関数(setUser)をセットで共有する型
type AuthValue = { user: User; setUser: (user: User) => void };

const AuthContext = createContext<AuthValue | null>(null);

// Provider は専用コンポーネントに切り出すと使いやすい
export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User>(null);
  return (
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
}

// useContext を包んだ独自フック。Provider 外での使用を弾ける
export function useAuth() {
  const value = useContext(AuthContext);
  if (value === null) {
    throw new Error('useAuth は AuthProvider の中で使ってください');
  }
  return value;
}

使う側は useAuth を呼ぶだけで、現在のユーザーと更新関数の両方を受け取れます。ログインボタンで setUser を呼べば、Provider の内側にある離れたコンポーネントの表示も一緒に切り替わります。

LoginButton.tsx
import { useAuth } from './AuthContext';

function LoginButton() {
  const { user, setUser } = useAuth();

  if (user) {
    return <button onClick={() => setUser(null)}>ログアウト</button>;
  }
  return (
    <button onClick={() => setUser({ name: 'たくみ' })}>
      ログイン
    </button>
  );
}

このように、更新関数も一緒に渡すことで、Context は「読み取り専用の設定値」だけでなく「アプリ全体で共有する状態」も扱えるようになります。テーマの切り替えも、同じ要領で themesetTheme をセットで渡せば実現できます。

使いすぎと再レンダリングの注意点

便利な Context ですが、注意したい点もあります。まず、value の値が変わると、その Context を useContext で読んでいるすべてのコンポーネントが再レンダリングされます。更新頻度の高い値を1つの大きな Context にまとめて入れてしまうと、関係のないコンポーネントまで頻繁に再描画され、パフォーマンスの低下につながります。頻繁に変わる値と滅多に変わらない値は、Context を分けて持つと影響範囲を狭められます。

もう一つ気をつけたいのが、value にオブジェクトをその場で作って渡すケースです。Provider が再レンダリングされるたびに value={{ user, setUser }}毎回新しいオブジェクトになるため、値の中身が同じでも再レンダリングのきっかけになります。影響が気になる場合は useMemo でオブジェクトを固定するとよいでしょう。また、Context は「本当に離れた場所で共有したい値」に使うのが基本です。親から子へ一段渡すだけなら素直に props を使うほうが読みやすく、なんでも Context に入れるとかえって依存関係が見えにくくなります。

まとめ

Context API と useContext は、props のバケツリレーを解消して、離れたコンポーネント間で値を共有するための仕組みです。createContext で Context を作り、<Context.Provider value={...}> で値を配り、useContext で受け取る、という3ステップが基本の流れでした。テーマやログインユーザーのようにアプリ全体で使う値は Context 向きで、更新関数も一緒に渡せば共有状態として扱えます。一方で、value の変化がすべての読み取り側の再レンダリングを引き起こす点や、なんでも Context に入れると読みにくくなる点には注意し、props と使い分けていくのがコツです。

参考ページ