typescript + nextjsをvercelにデプロイしたところ、ReactのuseContext
まわりの扱いでエラーに苦しんだので書きます
環境
- MacOS - AppleM1チップ - 16GBメモリ - Sonoma14.5
- npm -
v10.8.0
- React -
v^18.0
- Nextjs -
v14.2.4
Vercel - 発生したエラー
Buildでこちら2つのエラーが発生しました。
Build Logs
- Error:
- x You're importing a component that needs createContext. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
Build Logs
- Failed to compile.
app/page.tsx
- Type error: Page "app/page.tsx" does not match the required types of a Next.js Page.
- "Context" is not a valid Page export field.
ルートページのtsx
は以下のようにしていました
app/page.tsx
import { createContext, useState } from 'react';
import { Header, MainBody, ...} from '@components';
import { contextConfig, contextData } from 'context';
export const Context = createContext<contextConfig>({
context_data: {
// グローバルStateの変数の型
} as contextData,
setContextData: () => {}
});
export default function Home() {
const [context_data, setContextData] = useState({
// グローバルStateの変数の型
} as contextData);
return (
<Context.Provider value={{context_data, setContextData}}>
<Header />
<MainBody>
...
</MainBody>
</Context.Provider>
);
}
エラー1: 解決策 - use client
"use client"
を追記する
- Nextjsでコンポーネント間の依存関係(
props
やuseContext
での変数の受け渡しなど)が発生する場合、クライアント側での実行に制御する必要があります -
"use client"
が書かれたコンポーネントの子コンポーネントは全てクライアントで制御されることになるので、"use server"
も駆使する場合は注意が必要
createContext
の定義元で"use client"
を追記してやる
app/page.tsx
+ "use client"
import { createContext, useState } from 'react';
import { Header, MainBody, ...} from '@components';
import { contextConfig, contextData } from 'context';
export const Context = createContext<contextConfig>({
...
これで1つ目のエラーは解消されました。
エラー2: 解決策 - Provider
ページコンポーネントとProviderは分ける
- Vercel環境でのNextjsは、
app
以下のルーティング専用のtsxは、ページコンポーネント以外の型のexport
を受け付けないようです -
app/page.tsx
では、Context
オブジェクトもexport
してしまっているので、エラーが発生していました
Provier
コンポーネントとして分けてやる
provider.tsx
import { createContext, useState } from 'react';
import { contextConfig, contextData } from 'context';
export const Context = createContext<contextConfig>({
context_data: {
// グローバルStateの変数の型
} as contextData,
setContextData: () => {}
});
const Provider: React.FC<PropsWithChildren> = ({children}) => {
const [context_data, setContextData] = useState({
// グローバルStateの変数の型
} as contextData);
return (
<Context.Provider value={{context_data, setContextData}}>
{children}
</Context.Provider>
);
}
export default Provider;
app/page.tsx
import { Header, MainBody, ...} from '@components';
+ import Provider from '@components/provider';
export default function Home() {
return (
+ <Provider>
<Header />
<MainBody>
...
</MainBody>
+ </Provider>
);
}
これで2つ目のエラーも解消されました
補足
React - useContext
- グローバルStateを扱うhook
-
useState
は単一コンポーネント内でしか扱えず、子コンポーネントで使用する場合はprops
で渡す必要がある - 深いネスト関係にあるコンポーネントでは、最上部から最下部に変数を渡したいとしても、ネストを辿って
props
で渡し続けるしかない -
useContext
で作られた<context.Provider />
を使えば、Providerの子コンポーネントはどの位置であっても変数を使用&更新できる
component.tsx
import { createContext, PropsWithChildren, useState } from 'react';
// グローバルStateの変数の型
type contextData = {
prop1: string,
prop2: number,
prop3: string[]
}
// グローバルStateの変数と更新関数の型
type contextConfig = {
context_data: contextData
setContextData: React.Dispatch<React.SetStateAction<contextData>>
}
// Contextの作成
export const Context = createContext<contextConfig>({
context_data: { // デフォルト値
prop1: '',
prop2: 0,
prop3: []
} as contextData,
setContextData: () => {}
});
// Context.provider
const Provider: React.FC<PropsWithChildren> = ({children}) => {
const [context_data, setContextData] = useState({
prop1: '',
prop2: 0,
prop3: []
} as contextData);
return (
<Context.Provider value={{context_data, setContextData}}>
{children}
</Context.Provider>
);
}
export default Provider;