📝

【React修行日記】Context API + TypeScript 型の渡し方

に公開

学習の目的

  • Contextについて理解する
  • Context APIの使い方について理解する
  • Context APIとTypeScriptを使用してテーマ切替の実装ができるようになる

Contextとは

Reactでは通常、コンポーネント間でデータを受け渡すときにpropsを使い、親コンポーネントから子コンポーネントへ情報が伝達される。
しかし、階層が深くなると 「props drilling」と呼ばれる状態が発生しやすくなる。(情報のバケツリレー)
そこで、Contextを使用することによってあらかじめ上位コンポーネントで用意された値を、任意の子孫コンポーネントで参照できるようになる。

Contextを使用する前に検討すること(公式より)

Contextは便利だが、安易に使いすぎるとアプリの設計やコンポーネントの再利用性に影響する可能性がある。
公式でも、Contextを導入する前には検討すべきことが挙げられている。

  • まずはpropsで渡す
    • 数レベルの階層であれば、普通にpropsを渡す方がコードがシンプルで理解しやすい
    • どのコンポーネントがどのデータを使うかが明確になるので、メンテナンス性が高くなる
  • childrenとして渡す
    • 中間コンポーネントがただデータを下に流すだけになっている場合は、childrenを使ってコンポーネントを抽出すると良い
    • 例: <Layout><Posts posts={posts} /></Layout>のようにデータを必要なコンポーネントに直接渡す

上記の方法では解決できない場合や、グローバルに共有したい値がある場合にContextを使うのが適切。

https://ja.react.dev/learn/passing-data-deeply-with-context#before-you-use-context

Context APIとは

Context API は「コンポーネント間でデータを共有するためのReact公式の仕組み」。
基本的な流れは以下。

1. Contextの作成

まずは共有したい値の型を定義して、createContextでコンテキストを作る。

import { createContext } from "react";

const MyContext = createContext<string | null>(null);
  • MyContext は作成したContextオブジェクト
  • <MyContext>を使うことで値を提供したり、子コンポーネントで利用できる
  • 型を付けておくことで、TypeScriptでの型補完や安全性が確保される

2. Provider役のコンポーネントを用意

次に、Contextに渡す値をまとめて管理するラッパーコンポーネントを作る。

export function MyProvider({ children }: { children: React.ReactNode }) {
  const value = "Hello, Context!";

  return <MyContext value={value}>{children}</MyContext>;
}
  • MyProvider はContextに値を渡す役割を持つ
  • childrenをラップすることで、ツリー内のどの子孫コンポーネントでも値を利用可能
  • 状態や関数(useState / useReducer)をまとめて渡すこともできる

React18以前では<MyContext.Provider value={...}>のように書く必要があったが、React19以降は<MyContext value={...}>とContextオブジェクト自体をJSXコンポーネントとして使えるようになった。
なお、<MyContext.Provider>の記法も依然として動作するが、今後は新しい書き方が推奨されている。

https://ja.react.dev/blog/2024/12/05/react-19#context-as-a-provider

3. 子コンポーネントで利用

最後に、子コンポーネントでContextの値を参照する。

import { use } from "react";

function Child() {
  const ctx = use(MyContext);
  if (!ctx) throw new Error("MyContextが使える範囲ではありません");

  return <div>{ctx}</div>;
}
  • use(MyContext)でProviderから渡された値を取り出す
  • nullチェックを入れることで、Provider外での誤使用を防止できる
  • 取得した値は通常の変数として扱えるため、propsを経由せずに値にアクセス可能

React19以降ではuse(MyContext)で値を取り出せる。従来通りuseContext(MyContext)を使うことも可能だが、useはifやforといったブロック内でも呼び出せる点が特徴。

Context APIとTypeScriptを使用したテーマ切替

今回はContext APIを使用して簡易的なテーマ切替を実装してみた...!

※今回はテーマの切り替えが視覚的にわかりやすくなるよう、簡単なコンテンツを追加

Contextの作成

// ThemeContext.tsx
import { createContext } from "react";

export type Theme = "light" | "dark";

export type ThemeContextType = {
  theme: Theme;
  toggleTheme: () => void;
};

export const ThemeContext = createContext<ThemeContextType | null>(null);
  • Theme型でテーマの種類を "light" / "dark" に限定
  • ThemeContextTypeでContextが持つ値の形(themetoggleTheme)を定義
  • createContext<ThemeContextType | null>(null)
    → 初期値をnullにしておくことで、Providerの外でContextを使った場合にnullが返り、明示的にエラーを投げて検出できる

Providerコンポーネントの作成

// ThemeProvider.tsx
import { useState } from "react";
import { ThemeContext, type Theme } from "./ThemeContext";

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
  const [theme, setTheme] = useState<Theme>("light");

  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };

  return <ThemeContext value={{ theme, toggleTheme }}>{children}</ThemeContext>;
};
  • ThemeContext.Providerの代わりにThemeContextを直接JSXで使う
  • 状態管理(useState)はProvider内部で完結しているため、外部からも簡単に利用できる

テーマを使うコンポーネント

import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { ThemeContext } from "@/context/ThemeContext";
import { use } from "react";

export default function ThemeToggleDemo() {
  const ctx = use(ThemeContext);
  if (!ctx) throw new Error("Error");
  const { theme, toggleTheme } = ctx;

  return (
    <div
      className={`flex flex-col items-center gap-6 transition-colors p-8 ${
        theme === "light" ? "bg-white text-black" : "bg-gray-900 text-white"
      }`}
    >
      <h1 className="text-3xl font-bold">テーマ切替デモ</h1>

      {/* ボタン */}
      <Button
        onClick={toggleTheme}
        variant={theme === "light" ? "default" : "secondary"}
      >
        現在のテーマ: {theme} (クリックで切り替え)
      </Button>

      {/* サンプルコンテンツ */}
      <div className="flex flex-col md:flex-row gap-4">
        <Card
          className={`transition-colors ${
            theme === "light" ? "bg-white" : "bg-gray-800 text-white"
          }`}
        >
          <CardContent>Card: テーマ連動</CardContent>
        </Card>
      </div>
    </div>
  );
}

shadcn/uiを使用

  • use(ThemeContext)を使って値を取得
  • Providerの設定漏れを検出するためのエラーチェック
    →初期値をnullにしているため、Providerの外で利用された場合にエラーを投げて検知できるようにしている

今回は学習用のため、各コンポーネント内でテーマに応じてクラスを直接切り替えている。
実際のアプリ開発では、data-theme属性やCSS変数、Tailwindのdark:モードなどを併用すると、スタイル管理をより簡潔に保てる。

アプリ全体にProviderを適用

// App.tsx
import ThemeProvider from "@/context/ThemeProvider";
import ThemeToggleDemo from "@/components/ThemeToggleDemo";

function App() {
  return (
    <ThemeProvider>
      <ThemeToggleDemo />
    </ThemeProvider>
  );
}

export default App;
  • ルートでProviderをラップすることで、下層のコンポーネントすべてでテーマ情報が利用可能になる

まとめ

  • Contextはpropsのバケツリレーを解消し、コンポーネント間のデータ共有をシンプルにできる
  • ただし、使いすぎると再利用性を下げるため、まずはpropsやchildrenの利用を検討するのが望ましい
  • TypeScriptと組み合わせることで、型安全かつ保守性の高いグローバル状態管理が可能になる

参考

https://ja.react.dev/learn/passing-data-deeply-with-context

Discussion