App Routerのコンポーネント
Nextjsのバージョン13.4からApp Routerが安定版として利用が可能になりました。
App Routerではコンポーネントをクライアントコンポーネントとサーバーコンポーネントをうまく使い分ける必要があります。
クライアントコンポーネントはファイルの先頭に'use client'
と記述されたコンポーネントです。このコンポーネントはサーバー側で事前レンダリングがされ、クライアント側でコンポーネントが持つインターラクションが付与されます。
例えば以下のようなクライアントコンポーネントは
'use client';
export const SampleButton: FC = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count => count + 1)}>
{count}
</button>
);
};
事前レンダリングでサーバーから何の機能も持たない見た目だけのhtml要素がレンダリングされ
<button>0</button>
同時に送信されるJavaScriptファイルを読み込むことでカウンターとしての機能を持ったボタンとして利用できます(ハイドレーション)。
一方サーバーコンポーネントはApp Routerにおいてデフォルトのコンポーネントで、サーバー上で全て組み立てられます。サーバー上で組み立てることでクライアントに送信するJavaScriptのサイズを縮小することができるのでクライアントコンポーネントだけを利用した時と比較してパフォーマンスが向上します。そのような背景があり、コンポーネントのデフォルトがサーバーコンポーネントとなり、サーバコンポーネントとして扱えないものがあれば明示的にクライアントコンポーネントとして宣言させる仕様になっていると考えられます。
サーバーコンポーネントはサーバ上で完結する必要があるので、useState
などを用いたReactの状態管理を行えません。つまり、ReactのContext
やRecoilなどを用いたコンポーネント間の状態管理についてクライアントコンポーネント間だけで動かせるような工夫が必要となります。
App RouterでのRecoilの使い方
例があるとわかりやすいので、カウンター機能を持つ以下のようなPages Routerで構成されたNext.jsアプリケーションをApp Routerに移すことを考えます。
import { RecoilRoot } from "recoil"
export default function MyApp({ Component, pageProps }) {
return (
<RecoilRoot>
<Component {...pageProps} />
</RecoilRoot>
);
}
import { atom, useRecoilState } from "recoil";
const countAtom = atom({
key: 'count',
default: 0,
});
export const Counter: FC = () => {
const [count, setCount] = useRecoilState(countAtom);
return (
<button onClick={() => setCount(count => count + 1)}>
{count}
</button>
);
}
import { Counter } from '@/components/Counter'
export default function Home() {
return (
<div>
<p>カウンター</p>
<Counter />
<Counter />
</div>
);
}
これによって出力されるのは値を共有する二つのカウンターです。
まず、pages/_app.tsx
を移行します。
'use client'
import { ReactNode } from "react";
import { RecoilRoot } from "./recoil";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ja">
<body>
<RecoilRoot>{children}</RecoilRoot>
</body>
</html>
);
}
layout.tsx
でRecoilRoot
を使うのでクライアントコンポーネントとして定義しました。これによってApp Router内のコンポーネントでRecoilを用いた状態管理ができるようになります。
この例ではlayout.tsx
を自体をクライアントコンポーネントとして扱いましたが、Providerコンポーネントを別で作ってクライアントコンポーネントを最小にすることでパフォーマンスを突き詰められます。
'use client'
import { ReactNode } from "react";
import { RecoilRoot } from "./recoil";
export default function AppProvider({ children }: { children: ReactNode }) {
return (
<RecoilRoot>{children}</RecoilRoot>
);
}
import { ReactNode } from "react";
import { AppProvider } from "./provider";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ja">
<body>
<AppProvider>{children}</AppProvider>
</body>
</html>
);
}
次に、pages/counter.tsx
の移行を行います。
import { Counter } from '@/components/Counter'
export default function Home() {
return (
<div>
<p>カウンター</p>
<Counter />
<Counter />
</div>
);
}
'use client'
import { atom, useRecoilState } from "recoil";
const countAtom = atom({
key: 'count',
default: 0,
});
export const Counter: FC = () => {
const [count, setCount] = useRecoilState(countAtom);
return (
<button onClick={() => setCount(count => count + 1)}>
{count}
</button>
);
}
components/Counter.tsx
は状態を扱うコンポーネントであるためクライアントコンポーネントとして定義します。
ここまで変更したのちに、npm run dev
やnpm run build && npm run start
をするとPages routerの時と同じようなページを取得できます(--turbo
でも動きます)。
まとめ
App Routerの台頭でrecoilなどを用いてコンポーネント間で行う状態管理がクライアントコンポーネントの間にあるサーバーコンポーネントによって難しくなると考えていました。
- layout(サーバーコンポーネント)
- AppProvider(クライアントコンポーネント)
- page(サーバーコンポーネント)
- Counter(クライアントコンポーネント)
- Counter(クライアントコンポーネント)
しかし今回変更したアプリケーションのように、状態管理はクライアントコンポーネントだけ扱うというところを意識さえすればこれまでと変わりなく簡単に利用できました。
これからもrecoilを使った状態管理を楽しんでいきたいです。