デザインについての学習メモブログ

React入門 #16 – useContextでグローバル状態管理

記事内に広告が含まれています。

React入門 #16 – useContextでグローバル状態管理

これまでpropsを使ってデータを親から子へ渡してきましたが、深くネストされたコンポーネント間でデータを共有する場合、propsを何層にも渡す必要があります(Props Drilling)。

useContextは、この問題を解決し、グローバルな状態管理を可能にします。

この記事では、Context APIの使い方と実践的なパターンを詳しく学んでいきます。

Props Drillingの問題

Props Drilling がどのような問題を引き起こすのかを理解できます。

コンポーネントの階層が深くなると、必要ない中間コンポーネントにまで props を渡し続けなければならず、コードの保守性が下がってしまうことを体感できます。

問題のある例

ここでは、usersetUserApp → Layout → Sidebar → UserMenu と4層にわたって渡しています。

LayoutSidebar は実際には user を使わないにもかかわらず、受け渡しのためだけに props を持つ必要があります。

これが Props Drilling です。

TSX
// 深くネストされたコンポーネント構造
import { useState } from 'react'
import './App.css'
type User = {
  name: string
  theme: 'light' | 'dark'
}

function App() {
  const [user, setUser] = useState<User>({ name: '太郎', theme: 'light' })
  return <Layout user={user} setUser={setUser} />
}

function Layout({ user, setUser }: { user: User; setUser: React.Dispatch<React.SetStateAction<User>> }) {
  return <Sidebar user={user} setUser={setUser} />
}

function Sidebar({ user, setUser }: { user: User; setUser: React.Dispatch<React.SetStateAction<User>> }) {
  return <UserMenu user={user} setUser={setUser} />
}

function UserMenu({ user, setUser }: { user: User; setUser: React.Dispatch<React.SetStateAction<User>> }) {
  return (
    <div>
      <p>{user.name}</p>
      <p>{user.theme}</p>
      <button onClick={() => setUser({ ...user, theme: 'dark' })}>
        テーマ変更
      </button>
    </div>
  )
}

export default App
// userとsetUserを何層にも渡している(Props Drilling)

この問題をuseContextで解決します。

Contextの基本

Context API の基本的な使い方(作成・提供・取得)と、カスタムフックを使ってより安全に Context を利用するパターンが身につきます。

Props Drilling を解消し、必要なコンポーネントだけがデータに直接アクセスできる構造を作れるようになります。

createContextとuseContext

Context を使うには、大きく4つのステップがあります。

createContext で Context を作成

② Provider コンポーネントで値を提供、

③ カスタムフックでアクセスを安全にラップ(推奨)

④ 使いたいコンポーネントから呼び出す。③を省略して直接useContext(UserContext)でも受け取れる

という流れです。

これにより、中間コンポーネントに props を渡す必要がなくなります。

JSX
import { createContext, useContext, useState } from 'react'

// 1. Contextを作成
const UserContext = createContext()

// 2. Providerコンポーネントを作成
function UserProvider({ children }) {
  const [user, setUser] = useState({ name: '太郎', theme: 'light' })
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  )
}

// 3. カスタムフックを作成(オプションだが推奨)
function useUser() {
  const context = useContext(UserContext)
  if (!context) {
    throw new Error('useUserはUserProvider内で使用してください')
  }
  return context
}

// 4. 使用する
function App() {
  return (
    <UserProvider>
      <Layout />
    </UserProvider>
  )
}

function Layout() {
  return <Sidebar />  {/* propsを渡さない */}
}

function Sidebar() {
  return <UserMenu />  {/* propsを渡さない */}
}

function UserMenu() {
  const { user, setUser } = useUser()  {/* 直接アクセス */}
  
  return (
    <div>
      <p>{user.name}</p>
      <button onClick={() => setUser({ ...user, theme: 'dark' })}>
        テーマ変更
      </button>
    </div>
  )
}
export default App

処理の流れ

TypeScript版

TSX
import { createContext, useContext, useState, type JSX, type ReactNode } from 'react'

// 型定義
interface User {
  name: string
  theme: 'light' | 'dark'
}

interface UserContextType {
  user: User
  setUser: (user: User) => void
}

// 1. Contextを作成(デフォルト値はundefined)
const UserContext = createContext<UserContextType | undefined>(undefined)

// 2. Providerコンポーネント
interface UserProviderProps {
  children: ReactNode
}

function UserProvider({ children }: UserProviderProps): JSX.Element {
  const [user, setUser] = useState<User>({ name: '太郎', theme: 'light' })
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  )
}

// 3. カスタムフック
function useUser(): UserContextType {
  const context = useContext(UserContext)
  if (!context) {
    throw new Error('useUserはUserProvider内で使用してください')
  }
  return context
}

// 4. 使用
function UserMenu(): JSX.Element {
  const { user, setUser } = useUser()
  
  return (
    <div>
      <p>{user.name}</p>
      <button onClick={() => setUser({ ...user, theme: 'dark' })}>
        テーマ変更
      </button>
    </div>
  )
}

// アプリ本体。UserProviderでラップしてUserMenuを使用する
function App(): JSX.Element {
  return (
    <UserProvider>
      <UserMenu />
    </UserProvider>
  )
}

export default App

複数のContextを組み合わせる

認証・テーマ・通知など、複数の Context を組み合わせてアプリ全体を管理するパターンが身につきます。

Provider のネストが深くなりがちな問題を、AppProviders のような専用コンポーネントにまとめることで、すっきり整理する方法を学べます。

JavaScript版:

JSX
// App.jsx
import { AuthProvider } from './contexts/AuthContext'
import { ThemeProvider } from './contexts/ThemeContext'
import { NotificationProvider } from './contexts/NotificationContext'

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <NotificationProvider>
          <AppContent />
        </NotificationProvider>
      </ThemeProvider>
    </AuthProvider>
  )
}

// または、まとめて管理
function AppProviders({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </ThemeProvider>
    </AuthProvider>
  )
}

function App() {
  return (
    <AppProviders>
      <AppContent />
    </AppProviders>
  )
}

複数の Provider を組み合わせる際は、AppProviders コンポーネントに切り出しておくと、App.jsx をシンプルに保てます。

また、Provider の順序によって依存関係が生まれる場合(例:ThemeProvider が認証情報を使うなら AuthProvider の内側に置く)もあるため、順序には注意しましょう。

パフォーマンス最適化

Context を使ったアプリでよく起きる「不要な再レンダリング」の問題を防ぐ方法が身につきます。

useMemo による値のメモ化と、Context を適切に分割することで、変更に関係のないコンポーネントが再描画されないような設計ができるようになります。

useMemoでContextの値をメモ化

Provider の value に毎回新しいオブジェクトを渡すと、user が変わっていなくても子コンポーネントが再レンダリングされてしまいます。

useMemo を使って user が変わったときだけ新しいオブジェクトを生成することで、このムダを防げます。

JSX
import { createContext, useContext, useState, useMemo } from 'react'

function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  
  // 値をメモ化して不要な再レンダリングを防ぐ
  const value = useMemo(() => ({
    user,
    setUser,
    isAuthenticated: !!user
  }), [user])
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  )
}

Contextを分割する

すべての状態を1つの Context にまとめると、関係のない状態が変わっただけでも多くのコンポーネントが再レンダリングされます。

認証・テーマ・言語設定など「関心ごと」に Context を分割することで、それぞれの変更が影響するコンポーネントを最小限に抑えられます。

JSX
// ❌ 悪い例:すべてを1つのContextに
function AppProvider({ children }) {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('light')
  const [language, setLanguage] = useState('ja')
  
  // userが変わるだけでthemeやlanguageを使うコンポーネントも再レンダリング
  return (
    <AppContext.Provider value={{ user, setUser, theme, setTheme, language, setLanguage }}>
      {children}
    </AppContext.Provider>
  )
}

// ✅ 良い例:関心ごとに分割
function AppProviders({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LanguageProvider>
          {children}
        </LanguageProvider>
      </ThemeProvider>
    </AuthProvider>
  )
}

ベストプラクティス

Context を使う際に意識すべき設計上のポイントが身につきます。

「なぜカスタムフックが必要なのか」「デフォルト値はどう設定すべきか」「値のメモ化はどこでするか」といった、実務でよく悩むポイントを整理して理解できます。

1. カスタムフックを作る

useContext を直接呼び出すと、Provider の外で使われた場合に気づきにくいバグが発生します。

カスタムフックに切り出してエラーチェックを入れることで、問題の早期発見と分かりやすいエラーメッセージを実現できます。

❌ 悪い例:

JSX
function Component() {
  const context = useContext(MyContext)
  // エラーチェックがない
  return <div>{context.value}</div>
}

✅ 良い例:

JSX
function useMyContext() {
  const context = useContext(MyContext)
  if (!context) {
    throw new Error('useMyContextはMyProvider内で使用してください')
  }
  return context
}

function Component() {
  const { value } = useMyContext()
  return <div>{value}</div>
}

2. デフォルト値を適切に設定

createContext のデフォルト値を undefined にしておくことで、カスタムフック内のチェックが必ず機能し、Provider の外での誤った使用を防げます。

JSX
// デフォルト値はundefinedにして、エラーチェックを強制
const MyContext = createContext(undefined)

// または、型安全なデフォルト値を設定
const MyContext = createContext({
  value: '',
  setValue: () => {
    throw new Error('Providerの外で使用されています')
  }
})

3. 値の変更を最小限に

Provider の value にオブジェクトリテラルをそのまま渡すと、毎回新しい参照が生まれて不要な再レンダリングが起きます。

useMemo でメモ化することで、依存する値が変わったときだけ再レンダリングが走るようになります。

JSX
// ❌ 悪い例:毎回新しいオブジェクトを作成
<MyContext.Provider value={{ user, setUser }}>

// ✅ 良い例:useMemoでメモ化
const value = useMemo(() => ({ user, setUser }), [user])
<MyContext.Provider value={value}>

まとめ

この記事では、useContextを使ったグローバル状態管理を詳しく学びました。

重要なポイント:

  • createContextでContextを作成
  • Providerで値を提供
  • useContextで値を取得
  • カスタムフックでエラーチェック
  • useMemoでパフォーマンス最適化
  • 関心ごとに Context を分割

よく使うContextの例:

  • 認証(Auth)
  • テーマ(Theme)
  • 言語設定(Language/i18n)
  • 通知(Notification)
  • モーダル管理

ベストプラクティス:

  • カスタムフックを必ず作る
  • エラーチェックを実装
  • 値をメモ化する
  • Contextを細かく分割
  • TypeScriptで型を明示

次のステップ: 次回は、パフォーマンス最適化について学びます。React.memo、useMemo、useCallbackを使って、アプリケーションを高速化する方法を詳しく解説します!

useContextは小〜中規模のアプリケーションに最適です。

大規模アプリケーションでは、ReduxやZustandなどの状態管理ライブラリも検討しましょう!