React の Context についてちゃんと説明してみる。

## 概要

React には Context という Hook の仕組みがあるのですが、あまり使ってない人も多いようなので、解説していこうと思います。

Context の使う目的は基本的には props リレーを防ぐためとまとめていうことができますが、ライブラリ層とビジネスロジック層を分けたている場合に state をカプセル化したり公開したりすることにも使えまして、これが大変便利なのでこの辺も紹介していこうと思います。

順に説明します。


## 1) props リレーを防ぐ Context

一番基本的な使い方です。
state やコールバック等を下位コンポーネントの深い層にまで渡すとなると props のリレーが生まれて冗長な記述が連続して嫌な気持ちになりますね。
そこで Context の出番です。

```tsx:メンテナビリティが下がっている例
function App() {
  const user: User = { name: "John Doe", age: 30 }
  // user 渡す
  return <Parent user={user} />
}

// user を下位にリレーする（自分では使わない）
function Parent(props: { user: User }) {
  // user 渡す
  return <Child user={props.user} />
}

// user を下位にリレーする（自分では使わない）
function Child(props: { user: User }) {
  // user 渡す
  return <Grandchild user={props.user} />
}

// user 受け取って使う
function Grandchild(props: { user: User }) {
  return (
    <div>
      <h1>Name: {props.user.name}</h1>
      <p>Age: {props.user.age}</p>
    </div>
  )
}
```

↓ 下記のようにスッキリかけます。

```tsx:Context を使ってスッキリした例
const UserContext = createContext<{ user: User }>({ user: { name: '', age: 0 } })

function App() {
  const user = { name: "John Doe", age: 30 }
  return (
    // user 渡す
    <UserContext.Provider value={{ user }}>
      <Parent />
    </UserContext.Provider>
  )
}

// user の受け渡し不要
function Parent() {
  return <Child />
}

// user の受け渡し不要
function Child() {
  return <Grandchild />
}

function Grandchild() {
  // 使うところで受け取る
  const { user } = useContext(UserContext)
  return (
    <div>
      <h1>Name: {user.name}</h1>
      <p>Age: {user.age}</p>
    </div>
  )
}
```

中間層で無駄なリレーがなくなりましたね。
これなら将来 user のように受け渡す値の仕様が変わったら際にも、中間層の無駄な修正が発生しません。
メンテナビリティが高まります。


## 2) ライブラリ層でカプセル化

ソースの可読性を上げるコツとして、ビジネス要件から来る『ビジネスロジック層』と、システム的な便宜的な理由や全体統一の観点から来る『ライブラリ層』を分離することが重要です。

ライブラリ層が、親コンポーネントとその中で使うべき部品コンポーネントを両方提供することはよくありますね。

たとえば `<App></App>` と共にその中で使うべき `<CommonButton />` などのように。

その際にライブラリ層、つまり `<App></App>` と `<CommonButton />` との間でやり取りする state (や コールバック) が、ビジネス層からは見えないようにしたいときには Context を使います。

```tsx:ライブラリ層でカプセル化する例
// 非公開のコンポーネント
// ※ ここを露出 (export) させないことで、間に挟まるビジネスロジック層ではアクセスができません。
//    ビジネスロジック層では使ってはいけないのだと
//    （ライブラリ層の都合で仕様が変わるものであるのだと）明示できるのです。
const AppContext = createContext<{
  log: (message: string) => void
}>({
  log: () => {},
})

// 外部公開する『親』コンポーネント
// ※ Context の値を注入する
export function App(props: { children: ReactNode }) {
  const user: User = { name: "John Doe", age: 30 }

  const log = (message: string) => {
    console.log(`[${user.name}]: ${message}`)
  }

  return (
    <AppContext.Provider value={{ log }}>
      <div>
        <h1>Framework App</h1>
          {props.children}
        </div>
    </AppContext.Provider>
  )
}

// 外部公開する『部品』コンポーネント
// ※ Context の値を使う
export function CommonButton(props: { label: string; onClick: () => void }) {
  const { log } = useContext(AppContext)
  return (
    <button onClick={() => {
       log(`'${props.label}' Button was clicked`)
       props.onClick()
    }}>
      {props.label}
    </button>
  )
}
```

```tsx:利用箇所でカプセル化されている例
function UserApp() {
  // App と CommonButton 間で state やコールバックがやりとりされているが、
  // ビジネスロジック層からは見えない。
  return (
    <App>
      <CommonButton />
    </App>
  )
}
```

## 3) ライブラリ層からリアクティブな値や関数を供給する

ライブラリ層で統一して使うべき state やリアクティブな関数を用意したいことがありますね。
たとえば、前述のログを出すための `log` など。

基本的な実装方法は前述の例と同じですが、Context を公開する点と公開の際の粒度の考え方が大きく異なります。

どういうことかというと、Context を供給する側（ライブラリ層）と使う側（ビジネスロジック層）の層が異なる場合、どれくらいの頻度で値が更新されるか（＝再レンダリングされるか）がカプセル化されて隠されてしまいます（本来隠されていても良いはずです）。

使うが側（ビジネスロジック層）にしてみると、`const { aaa } = useContext(XxxContext)` のように `XxxContext` が供給してくれる値のうち `aaa` しか使ってないわけですが、実際には `XxxContext` が供給するいずれの値 `bbb` や `ccc` が更新されたときでも use しているビジネスロジック層のコンポーネントは再レンダリングされることになります。これは想像以上にパフォーマンス問題や謎のチラつき問題を引き起こすかもしれないのです。

つまり、全部入りの Context を用意して使いたいものだけ、`const { aaa } =` で使ってね、という設計は最適とはいえないのです。
過剰にレンダリングされる危険があるのです。

なので、ライブラリ層の Context の設計では再レンダリングが最小となるような単位（＝更新サイクルが同一になるようなまとまり）で Context を分けるよう配慮すべきなのです。

また、分けることでビジネスロジック層の単体テストを書く際にも Context を Mock するのが簡単になる点も大きいです。

```ts
export const LogContext = createContext<{
  log: (message: string) => void
}>({
  log: () => {},
})
```

## ここまでのまとめ

Context の基本的な使い方と、それらをライブラリ層・ビジネスロジック層で分けて使うポイントを見てきました。
ここからはテストする方法について見ていきましょう。


## テストパターン1) Context をコンポーネントで使う場合

Context を使っている部分は `useContext` を使ってるわけですが、普通の Hook のようには Mock しづらいので特別な方法を取ります。

```tsx
render(
  <MyContext.Provider value={mockContextValue}>
    <MyComponent />
  </MyContext.Provider>
)
```
でテスト用の Context 値を注入します。
具体的には下記のようになります。

```tsx
// テスト対象のコンポーネント
export const MyComponent = () => {
  const { value, doSomething } = useContext(MyContext)
  return (
    <div>
      <p>{value}</p>
      <button onClick={doSomething}>Click me</button>
    </div>
  )
}

// テストコード
describe('MyComponent', () => {
  it('Context の内容で表示・実行されること', () => {
    // モックされた Context の値を定義
    const mockContextValue: ContextType<typeof MyContext> = {
      value: 'Mocked Value',
      doSomething: vi.fn(),
    }

    // コンポーネントをモック Context でラップしてレンダリング
    render(
      <MyContext.Provider value={mockContextValue}>
        <MyComponent />
      </MyContext.Provider>
    )

    // モック値がレンダリングされているか確認
    expect(screen.getByText('Mocked Value')).toBeInTheDocument()

    // ボタンをクリックしてモック関数が呼び出されたか確認
    fireEvent.click(screen.getByText('Click me'))
    expect(mockDoSomething).toHaveBeenCalledTimes(1)
  })
})
```


## テストパターン2) Context をカスタム Hook (use から始まる関数) で使う場合

カスタム Hook のテストでは render で注入するのは冗長ですね。もっと最適な方法があります。
`renderHook` に `wrapper` を渡します。
具体的には下記のようになります。

```tsx
// テスト対象のコンポーネント
export const useXxx = () => {
  const { value, doSomething } = useContext(MyContext)
  return {
    xxx: value + 1,
    yyy: doSomething(),
  }
}

// テストコード
describe('useXxx', () => {
  it('Context の内容が使われること', () => {
    // モックされた Context の値を定義
    const mockContextValue: ContextType<typeof useXxx> = {
      value: 11,
      doSomething: vi.fn().mockReturnValue(22),
    }

    // コンポーネントをモック Context でラップしてレンダリング
    const wrapper = (props: { children: ReactNode }) => (
      <MyContext.Provider value={mockContextValue}>
        {props.children}
      </MyContext.Provider>
    )
    const { result } = renderHook(() => useXxx(), { wrapper })

    // モック値の確認
    expect(result.current).toEqual({ xxx: 11, yyy: 22 })
    expect(mockContextValue.doSomething).toHaveBeenCalledTimes(1)
  })
})
```

## まとめ

Context の使い方とテストの仕方を見てきました。
何か質問・ご意見などあれば気軽に聞いてくださいね。

それでは。