Next.jsのAppRouterを使い、ClientComponentsのみで非同期データのSSRを行う
最初に
今回のサンプルコードのリンク
AppRouter使用時の一般的な非同期データのSSR
AppRouterで非同期データを含んだSSRを行う場合、ServerComponentsでデータを取得して自身でレンダリングするか、それをClientComponentsに渡すかする必要があります。ブラウザ側でユーザの入力に合わせて柔軟に表示内容を変えようとするとServerComponentsとClientComponentsのやり取りをそれぞれ記述しなければならず、PagesRouter上でgetServerSidePropsからデータを渡していたときと労力に差がありません。
ClientComponentsで非同期データを扱えば、コードの記述が楽になる
ServerComponentsを使えば、その部分のコンポーネントのjsコードをクライアントに渡す必要がなくなり、少しだけ通信量が節約できます。ただ、微々たるものです。そのためにデータ取得用コンポーネントとUI用コンポーネントを分離するのは手間になります。だったらClientComponentsで非同期データを扱えるようにすれば良いのです。
必要なもの
Next.jsでSSRを簡単に行うためのパッケージ
https://www.npmjs.com/package/next-ssr
以下のような形でNext.js環境下に追加でパッケージを入れます。
1yarn add next-ssr
単純な非同期SSRのサンプル
src/app/layout.tsx
SSR用のデータを取り扱うProviderをlayoutに設置します。これで配下のコンポーネントから、簡単SSR機能が使えるようになります。
1import { SSRProvider } from 'next-ssr';23export const metadata = {4 title: 'samples of next-ssr',5 description: 'SSR with AppRouter.',6};78export default function RootLayout({ children }: { children: React.ReactNode }) {9 return (10 <html lang="en">11 <body>12 <SSRProvider>{children}</SSRProvider>13 </body>14 </html>15 );16}
src/app/simple/page.tsx
非同期データを扱うための最小限のコードです。非同期関数から'Hello world!'を渡しています。もちろんfetchなどで取得したデータも使えます。
1'use client';2import { useSSR } from 'next-ssr';34/**5 * Page display components6 */7const Page = () => {8 const { data } = useSSR(async () => 'Hello world!');9 return <div>{data}</div>;10};11export default Page;
出力されたHTML
bodyタグの直後に、<div>Hello world!</div>が入っています。非同期データのSSRに成功しています。
天気予報のサンプル
気象庁のサイトからデータをfetchして内容を表示しています。SSR後はClientComponentsなので、UIイベントで再ロードも行えます。その際はクライアント側でfetchが行われます。
src/app/weather/page.tsx
1'use client';2import { useSSR } from 'next-ssr';34export interface WeatherType {5 publishingOffice: string;6 reportDatetime: string;7 targetArea: string;8 headlineText: string;9 text: string;10}1112/**13 * Data obtained from the JMA website.14 */15const fetchWeather = (id: number): Promise<WeatherType> =>16 fetch(`https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${id}.json`)17 .then((r) => r.json())18 .then(19 // Additional weights (500 ms)20 (r) => new Promise((resolve) => setTimeout(() => resolve(r), 500))21 );2223/**24 * Components for displaying weather information25 */26const Weather = ({ code }: { code: number }) => {27 const { data, reload, isLoading } = useSSR<WeatherType>(() => fetchWeather(code), { key: code });28 if (!data) return <div>loading</div>;29 const { targetArea, reportDatetime, headlineText, text } = data;30 return (31 <div style={isLoading ? { background: 'gray', position: 'relative' } : undefined}>32 {isLoading && (33 <div style={{ position: 'absolute', color: 'white', top: '50%', left: '50%' }}>loading</div>34 )}35 <h1>{targetArea}</h1>36 <button onClick={reload}>Reload</button>37 <div>38 {new Date(reportDatetime).toLocaleString('ja-JP', {39 timeZone: 'JST',40 })}41 </div>42 <div>{headlineText}</div>43 <div style={{ whiteSpace: 'pre-wrap' }}>{text}</div>44 </div>45 );46};4748/**49 * Page display components50 */5152const Page = () => {53 return (54 <>55 {/* Chiba */}56 <Weather code={120000} />57 {/* Tokyo */}58 <Weather code={130000} />59 {/* Kanagawa */}60 <Weather code={140000} />61 </>62 );63};64export default Page;65
出力されたHTML
天気予報の内容がSSRされています。
まとめ
AppRouterの本来の機能をガン無視しています。そのためpage.tsxに記述している内容は、PagesRouter上にそのまま持って行ってもファイル名だけ直せば同じ動きをします。
そもそのもところで、Next.jsがReactのレンダリングに使っているreact-dom/serverのrenderToPipeableStreamは、コンポーネントのthrow promiseで非同期待ちしてくれるので、ServerComponentsとClientComponentsの種別に関わりなく非同期待ちが可能です。あとは必要に応じて、データを配るコードを足してやれば便利に動くようになります。
ちなみにここのBlogシステムは今回紹介した動作原理をUrqlのプラグインとして実装して運用していますが、コードを書くのも楽だし、特になんの問題もなくキビキビ動いています。
PagesRouter版
https://github.com/SoraKumo001/md-blogAppRouter版
https://github.com/SoraKumo001/md-blog/tree/AppRouter
ほとんどコードに違いがないので、PagesからAppの移植は簡単です。ただ、ページ遷移時にPagesの方がファイルの読み取り頻度が低いので、最終的にPagesのままにするという結論に至りました。ちなみにPagesRouter版でもAppRouterをAPIRouteの代替としては使ってます。
今のところAppRouterにメリットは感じないので趣味でいじりつつ、しばらくは様子見したいと思っています。