はじめに
Next.js を使っていると、サーバーサイドとクライアントサイドの違いや、それぞれの制約に引っかかることが多いです。特に、状態管理で useContext を使おうとしたとき、サーバーサイドのファイルとどう整合性を取るべきかで沼りました。この記事は、そんな状況を整理して備忘録的にまとめたものです。
開発中に直面した悩み
1: useContext でHeader.tsxとpage.tsxの状態を共有したい
2: サーバーサイドで metadata を使って SEO 情報を設定しているため、RootLayoutをサーバーサイドにしておきたい。
なぜ困ったのか
metadata はサーバーサイドで生成される必要があるので、RootLayout で use client をつけられません。でも、RootLayout で全体の状態を管理しようと思って useContext を使いたい場合、useState などの React Hooks を使えないのがネックでした。
問題のコード
const metadata = {
title: 'My App',
description: 'this is Next.js app',
};
export default function RootLayout({ children }) {
const [state, setState] = useState('');
const stateInfo = {
state,
setState,
};
const AppStateContext = createContext(stateInfo);
return (
<html>
<AppStateContext.Provider value={AppStateContext}>
<body>{children}</body>
</AppStateContext.Provider>
</html>
);
}
しかしuse client をつけると metadata が使えなくなる。
解決策: 状態管理を分離する
結論として、状態管理は RootLayout ではなく、別のクライアントサイドのProviderコンポーネントを作成して、任せることにしました。
実装例
状態管理用の Provider を作る
'use client';
import React, { createContext, ReactNode, useState } from 'react';
export const AppStateContext = createContext<ProviderContext | undefined>(
undefined
);
type ProviderContext = {
state: number;
setState: React.Dispatch<React.SetStateAction<number>>;
};
export function Provider({ children }: { children: ReactNode }) {
const [state, setState] = useState(0);
const stateObject = {
state,
setState,
};
return (
<AppStateContext.Provider value={stateObject}>
{children}
</AppStateContext.Provider>
);
}
export default Provider;
RootLayout に組み込む
import { AppStateProvider } from './Provider';
export const metadata = {
title: 'My App',
description: 'A simple Next.js app',
};
export default function RootLayout({ children }) {
return (
<html lang='en'>
<Provider>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Header />
{children}
</body>
</Provider>
</html>
);
}
Page での利用例
'use client';
import React, { useContext } from 'react';
import { AppStateContext } from '../Provider';
const Page = () => {
const providedObject = useContext(AppStateContext);
const plus = () => {
providedObject?.setState((prev) => prev + 1);
};
return (
<div>
<h1>Aのページ</h1>
<h1>{providedObject?.state}</h1>
<button onClick={plus}>plus</button>;
</div>
);
};
export default Page;
Header での利用例
'use client';
import React, { useContext } from 'react';
import { AppStateContext } from './Provider';
const Header = () => {
const providedObject = useContext(AppStateContext);
const plus = () => {
providedObject?.setState((prev) => prev + 1);
};
return (
<>
<h1>{providedObject?.state}</h1>
<button onClick={plus}>plus</button>;
</>
);
};
export default Header;
結果
Header.tsx と page.tsx の両方から state を操作でき、どちらの操作もリアルタイムに反映されました。
まとめ
専用の Provider を作ることで、状態管理を分離し、サーバーサイドとクライアントサイドの制約を回避できた
終わりに
この記事は、私自身の学習過程での気づきを整理した備忘録です。
もし内容に誤りや補足すべき点があれば、ぜひコメントで教えていただけると幸いです。
参考記事