Next.js × SPA
Next.js を使って SPA を作る。


はじめに
みなさんお久しぶりです!Web エンジニアの yoshimo(nakamoto)です。
今回は SPA とは何かと Next.js を使って SPA を作るときの要点を共有します。
SPA(Single-Page Application)
SPA はまず一つの HTML(index.html)と JavaScript(main.js)を返し、そのあとの処理はいずれも CSR(Client-side Rendering)で行われます。しかし、従来は React で SPA のアプリケーションを作るときに、大きく2つの問題がありました。1つ目は、全てのページの JavaScript を一気に読み込むととても重くなるという問題です(小規模なアプリなら問題なくとも、大規模なアプリだととても重くなります)。2つ目の問題はクライアントサイドのウォーターフォールです。つまり、リクエストがまずあって、その結果をもとにまたリクエストが行われるため、パフォーマンスが悪いです(まず上のコンポーネントがレンダリングされたら、次のコンポーネントがデータフェッチする)
Next.js(App Router)
上記の問題を解決するために React Router や Next.js などのフレームワークが重要になります。まず Next.js はルートごとに自動で JavaScript バンドルを分割し(チャンキングと呼ばれる)、複数の HTML エントリポイントを生成します。それによって、ユーザーは Web アプリを見るときに一つの HTML とその JavaScript を読み込むことになるため、1つ目の問題は解消します。
次に Next.js は データフェッチを親コンポーネント(or ルートレイアウト)で行なって Promise を返し、その値を use hook によってクライアントコンポーネントに展開することを推奨しています。データフェッチを親コンポーネント(or ルートレイアウト)にリフトアップすると、サーバーサイドで早めにデータフェッチを開始し、クライアントサイドに直ぐにレスポンスのストリーミングを開始できるということですね。
それによって、2つ目の問題(クライアントサイドのウォーターフォール)は解消されます。さらにクライアントとサーバー間のラウンドトリップも減りますし、サーバーは DB の近く(理想的には同じ場所)にあるので、パフォーマンスが大幅に向上します。
import { UserProvider } from './user-provider'
import { getUser } from './user' // some server-side function
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
let userPromise = getUser() // do NOT await
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
注)ルートレイアウトで Promise を取得するのに await は使いません。
(await を使ったらブロッキングが走ってしまうため)
'use client';
import { use, createContext, ReactNode } from 'react';
type User = any;
type UserContextType = {
user: User;
};
const UserContext = createContext<UserContextType | null>(null);
export function UserProvider({
children,
userPromise,
}: {
children: ReactNode;
userPromise: Promise<User | null>;
}) {
let initialUser = use(userPromise);
return (
<UserContext.Provider value={{ user: initialUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser(): UserContextType {
let context = use(UserContext);
if (context === null) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
'use client'
import { useUser } from './user-provider'
export function Profile() {
const { user } = useUser()
return '...'
}
補足)ちなみに Promise を使ってるコンポーネントは Suspense され、部分的にハイドレーションできる為、JavaScript のローディングが完了する前にストリーミング済み・プリレンダリング済みの HTML は見れます。
ただデータフェッチは、先述の方法よりも SWR によって SWRConfig の fallback(初期データ)を使った方がとても簡単に書けそうです(https://nextjs.org/docs/app/building-your-application/upgrading/single-page-applications#spas-with-swr)
参照
https://nextjs.org/docs/app/building-your-application/upgrading/single-page-applications