💡
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>
);
}
このとき、Header
や CounterDisplay
も "use client"
を付けなくていいと思っていた。
3. 実際の挙動
-
Header
はただの表示用 Server Component → 問題なし - でも
CounterDisplay
はuseContext()
を使っている -
"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