💡

Contextの子がクライアントコンポーネントにならない理由

に公開

1. はじめに

Next.js の App Router を使っていて、Context を提供する Provider コンポーネントに "use client" を付けたにも関わらず、その中の子コンポーネントでは React hooks(useContext, useState など)が使えないというエラーに遭遇した。

自分は最初、こう思っていた:

「Provider に "use client" を付けたんだから、その中にある子も全部クライアントで動くはずだろう」と。

でも実際にはそうはならなかった。この記事では、この挙動がなぜ起きるのかどこに誤解があったのかを、自分の体験とともに整理する。


2. 誤解していたこと

App Router で Context を提供する以下のようなコードを書いたとき:

// CounterContext.tsx
"use client";

export const CounterContext = createContext(...);

export function CounterProvider({ children }: { children: React.ReactNode }) {
  return (
    <CounterContext.Provider value={...}>
      {children}
    </CounterContext.Provider>
  );
}

この CounterProvider に囲まれる子コンポーネントたちは、全部クライアントとして動作すると思っていた。

たとえば layout.tsx 側では:

export default function RootLayout() {
  return (
    <CounterProvider>
      <Header />
      <CounterDisplay />
    </CounterProvider>
  );
}

このとき、HeaderCounterDisplay"use client" を付けなくていいと思っていた。


3. 実際の挙動

  • Header はただの表示用 Server Component → 問題なし
  • でも CounterDisplayuseContext() を使っている
  • "use client" を付けていないと、エラーになる(hookは Server Component では使えないため)
// CounterDisplay.tsx
import { useContext } from "react"; // ❌ エラーになる
import { CounterContext } from "./CounterContext";

export function CounterDisplay() {
  const [count, increment] = useContext(CounterContext);
  ...
}

このとき自分はこう思った:

「え、Providerが "use client" なら、その中にある子も全部クライアントになるんじゃないの?」


4. どこが間違いだったのか?

"use client" のスコープは「ファイル単位」

  • "use client" は、そのファイル自身をクライアントコンポーネントとして扱うためのフラグ
  • それが他のファイルに 伝播することはない
  • 親が "use client" でも、子が別ファイルなら、その子はサーバーとして扱われる

5. 親が"use client"でも、子は直接importされなければクライアントにならない

以下のように "use client" を書いたファイルで 直接 import されたモジュールは、クライアントバンドルに巻き込まれる:

// ClientParent.tsx
"use client";

import { Button } from "./Button"; // ✅ Button はクライアントになる

export function ClientParent() {
  return <Button />;
}

一方、children に渡しただけの子はクライアント扱いされない:

// ClientWrapper.tsx
"use client";

export function ClientWrapper({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>; // ❌ 子は巻き込まれない
}

6. 実際の例(今回の理解に必要な最小コード)

// CounterContext.tsx
"use client";

import { createContext, useState } from "react";

export const CounterContext = createContext<[number, () => void]>([0, () => {}]);

export function CounterProvider({ children }: { children: React.ReactNode }) {
  const [count, setCount] = useState(0);
  return (
    <CounterContext.Provider value={[count, () => setCount((c) => c + 1)]}>
      {children}
    </CounterContext.Provider>
  );
}
// layout.tsx
import { CounterProvider } from "./CounterContext";
import { Header } from "./Header"; // Server Component
import { CounterDisplay } from "./CounterDisplay"; // クライアントで動かしたい

export default function RootLayout() {
  return (
    <CounterProvider>
      <Header />
      <CounterDisplay />
    </CounterProvider>
  );
}
// CounterDisplay.tsx
"use client";

import { useContext } from "react";
import { CounterContext } from "./CounterContext";

export function CounterDisplay() {
  const [count, increment] = useContext(CounterContext);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}

7. まとめ

  • "use client" は「ファイル単位」で効く
  • 親がクライアントコンポーネントでも、子は import されない限り巻き込まれない
  • children に渡すだけでは Server のまま
  • useContext などの React hooks を使いたい場合は、そのファイルに明示的に "use client" を書く必要がある

8. おわりに

この仕様は理解してしまえばシンプルだけど、React の「親がクライアントだから子もクライアントでしょ」という従来の感覚とはズレているので、App Router では特に注意が必要。

Discussion