[React講座] コンテキストとグローバル状態

当ページのリンクには広告が含まれています。
目次

コンテキストとグローバル状態

なぜContextが必要になるのか?(Prop Drilling問題)

まず前提の「つらい状態」から。

よくある構造:

App
 ├─ Layout
 │   └─ Sidebar
 │       └─ UserPanel
 │           └─ UserAvatar

ここで、「ログイン中ユーザー情報 currentUserUserAvatar で表示したい」という状況を考えます。

素直にやると:

App(currentUserを持つ)
  ↓ props
Layout(currentUser)
  ↓ props
Sidebar(currentUser)
  ↓ props
UserPanel(currentUser)
  ↓ props
UserAvatar(currentUser) ← ここでやっと使う

みたいに、中間コンポーネントは使わないのに「ただ渡すだけ」 になりがちです。
これがいわゆる Prop Drilling(プロップスの穴掘り)です。

この問題を解消するために出てくるのが Context です。

Contextの基本

Contextとは、「木の上の方に“倉庫(Provider)”を置いて、その下のコンポーネントからは、どこからでも useContext で取り出せる仕組み」です。

使用するのは以下の3つ:

  1. createContext … コンテキストを定義
  2. Context.Provider … 値を配る側
  3. useContext(SomeContext) … 値を受け取る側

図にすると:

<UserContext.Provider value={user}>
  App
   ├─ Layout
   │   └─ Sidebar
   │       └─ UserPanel
   │           └─ UserAvatar   ← useContext(UserContext) で user を直接取得
</UserContext.Provider>

SidebarUserPanel は propsで受け取らなくてよくなる のがポイントです。

「テーマ切り替え」の例で理解する

一番シンプルなContextの題材としてよく出るのが「ライト/ダークテーマ」です。

① Contextの定義

// src/ThemeContext.jsx
import { createContext } from 'react'

export const ThemeContext = createContext('light')
// 引数は「デフォルト値」。Provider で包まなかったときに使われる値

② Providerでアプリ全体を包む

// src/App.jsx
import { useState } from 'react'
import { ThemeContext } from './ThemeContext'
import Page from './Page'

export default function App() {
  const [theme, setTheme] = useState('light')

  function toggleTheme() {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
  }

  return (
    <ThemeContext.Provider value={theme}>
      <div>
        <button onClick={toggleTheme}>
          テーマ切り替え(現在: {theme})
        </button>
        <Page />
      </div>
    </ThemeContext.Provider>
  )
}
  • ThemeContext.Providervaluetheme を渡している
  • このProvider「より下」にあるコンポーネントは、どこからでもこの theme を読める

③ 子コンポーネントから useContext で受け取る

// src/Page.jsx
import Content from './Content'

export default function Page() {
  return (
    <div>
      <h1>ページタイトル</h1>
      <Content />
    </div>
  )
}
// src/Content.jsx
import { useContext } from 'react'
import { ThemeContext } from './ThemeContext'

export default function Content() {
  const theme = useContext(ThemeContext)

  const style = {
    padding: '16px',
    backgroundColor: theme === 'light' ? '#fff' : '#333',
    color: theme === 'light' ? '#000' : '#fff',
  }

  return (
    <div style={style}>
      <p>このボックスはテーマによって色が変わります。</p>
    </div>
  )
}

ポイント整理:

  • useContext(ThemeContext) で、「今の value」がそのまま取れる
  • 間に Page があろうが Layout があろうが関係ない
  • どこから読んでも同じ theme state に紐づいている

これで「ルートでクリックしたテーマ切替ボタン」が、深い階層のコンポーネントの背景色まで一気につながります。

もう少し実務寄りの例:UserContext(ログインユーザー)

次は、より「実戦」でよく使う ログインユーザー情報 の例です。

① Context定義+Providerを分離しておく

// src/UserContext.jsx
import { createContext, useContext, useState } from 'react'

const UserContext = createContext(null)

// Providerコンポーネントを用意しておくと再利用しやすい
export function UserProvider({ children }) {
  const [user, setUser] = useState(null)

  // ログイン・ログアウト用の関数もここにまとめておける
  const login = (name) => {
    setUser({ name })
  }

  const logout = () => {
    setUser(null)
  }

  const value = { user, login, logout }

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}

// カスタムフックにしておくと呼び出し側が楽
export function useUser() {
  const ctx = useContext(UserContext)
  if (!ctx) {
    throw new Error('useUser must be used within UserProvider')
  }
  return ctx
}
  • UserProvider に以下を全部集約して、value で配る
    • user state
    • login 関数
    • logout 関数
  • 受け取る側は useUser() だけ呼べばOK、という形にしておくと使い勝手がよい

② アプリのルートで UserProvider で包む

// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { UserProvider } from './UserContext'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <UserProvider>
      <App />
    </UserProvider>
  </React.StrictMode>,
)

③ どこからでも useUser() でユーザー情報を取得・更新

ログインフォーム:

// src/LoginForm.jsx
import { useState } from 'react'
import { useUser } from './UserContext'

export default function LoginForm() {
  const [name, setName] = useState('')
  const { login } = useUser()

  function handleSubmit(e) {
    e.preventDefault()
    if (!name.trim()) return
    login(name.trim())
    setName('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前を入力"
      />
      <button type="submit">ログイン</button>
    </form>
  )
}

ナビバー:

// src/NavBar.jsx
import { useUser } from './UserContext'

export default function NavBar() {
  const { user, logout } = useUser()

  return (
    <header>
      <span>My App</span>
      <span style={{ marginLeft: '1rem' }}>
        {user ? `${user.name} さんとしてログイン中` : '未ログイン'}
      </span>
      {user && (
        <button onClick={logout} style={{ marginLeft: '1rem' }}>
          ログアウト
        </button>
      )}
    </header>
  )
}

App.jsx

// src/App.jsx
import NavBar from './NavBar'
import LoginForm from './LoginForm'

export default function App() {
  return (
    <div>
      <NavBar />
      <hr />
      <LoginForm />
      {/* 他のコンポーネントも全部 UserProvider の傘の下 */}
    </div>
  )
}

ここで何が嬉しいかというと:

user 情報を どのコンポーネントにも props で渡していない のに、NavBar でも LoginForm でも、その下層でもconst { user } = useUser() で同じログイン状態を参照できる

というところです。

「なんでもかんでもContext」はNG:使いどころの指針

Contextは便利ですが、何でもContextにすると逆に読みにくいコードになります。

Contextに向いているもの

  • アプリ全体で共有される「設定」「環境」系
    • テーマ(light/dark)
    • ロケール(ja/en)
    • 現在ログイン中のユーザー
    • 認可情報(権限)
  • 多くのコンポーネントが参照する「単一の真実」
    • 選択中のプロジェクト
    • 選択中の会社・チーム

Contextに向いていないもの

  • ある1画面の中だけで完結する状態
    • その画面のフォーム入力値
    • その画面だけのフィルタ状態
  • 頻繁に更新される大量データ(Context全ツリーが再レンダリングされやすい)

基本ルール

  • まずは「普通に state + props」で設計する
  • 「Prop Drilling がつらくなってきたな…」と思う箇所にだけContextを導入する

という流れが安全です。

Context使用時のパフォーマンス注意

1つのContextに何でも詰め込むと、「Contextの value が変わるたびに、そのContextを読んでいるコンポーネントが全部再レンダリングされる」という状態になりがちです。

対処策の例:

  • Contextを分ける
    • 例:AuthContextThemeContext を分ける
  • valueuseMemo でラップ

簡単な例:

const value = useMemo(() => ({ user, login, logout }), [user])

user が変わったときだけ、新しいオブジェクトを作るようにして、
不要な再レンダリングを減らすテクニックです。

最初はそこまで神経質にならなくてOKですが、「Contextのvalueが変わると、useContextで読んでいるコンポーネントは再描画される」ということだけ頭の片隅に置いておくと、後でパフォーマンスを考えるときに役立ちます。

まとめ

  • Propsを何階層も渡していく「Prop Drilling」を解消するためにContextがある
  • createContextProvider(値を配る) ⇒ useContext(値を読む)が基本フロー
  • テーマやログインユーザーのような「アプリ全体で共有される状態」はContextに向いている
  • UserProvider + useUser のように、「Provider+カスタムフック」という形でラップすると使いやすい
  • なんでもかんでもContextに入れないこと
    • まずは state + props で設計
    • 本当に「グローバルに共有したいもの」だけContext化

<<前へ(ルーティング(React Router))

>>次へ(useReducerとロジックの整理)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次