Next.jsでシングルページアプリケーションを構築する方法
Next.jsはシングルページアプリケーション(SPA)の構築に完全に対応しています。
これには、プリフェッチを用いた高速なルート遷移、クライアント側のデータ取得、ブラウザAPIの使用、サードパーティのクライアントライブラリとの統合、静的ルートの作成などが含まれます。
既存のSPAがある場合、コードに大きな変更を加えることなくNext.jsに移行できます。その後、必要に応じてサーバー機能を段階的に追加できます。
シングルページアプリケーションとは
SPAの定義はさまざまです。ここでは「厳密なSPA」を以下のように定義します。
- クライアント側レンダリング(CSR):アプリケーションは1つのHTMLファイル(例えば
index.html)で提供されます。すべてのルート、ページ遷移、データ取得はブラウザのJavaScriptで処理されます。 - 全ページリロードなし:各ルートで新しいドキュメントをリクエストする代わりに、クライアント側のJavaScriptが現在のページのDOMを操作し、必要に応じてデータを取得します。
厳密なSPAではページがインタラクティブになる前に大量のJavaScriptをロードする必要があることが多いです。さらに、クライアントデータのウォーターフォールは管理が難しくなる場合があります。Next.jsでSPAを構築することでこれらの問題を解決できます。
SPAでNext.jsを使用する理由
Next.jsはJavaScriptバンドルを自動的にコード分割し、異なるルートへの複数のHTMLエントリポイントを生成できます。これにより、クライアント側で不要なJavaScriptコードのロードを避け、バンドルサイズを削減して、ページロード速度を上げることができます。
next/linkコンポーネントは自動的にルートをプリフェッチし、厳密なSPAの高速なページ遷移を実現しながら、アプリケーションのルーティング状態をURLに保持するという利点があり、リンク共有が可能になります。
Next.jsは静的サイトとして、または単なる厳密なSPAとして始めることができ、すべてがクライアント側でレンダリングされます。プロジェクトが成長するにつれて、Next.jsは必要に応じてより多くのサーバー機能(例えばReact Server Components、Server Actionsなど)を段階的に追加できます。
例
SPAを構築するために使用される一般的なパターンと、Next.jsがそれらをどのように解決するかを見てみましょう。
Context Providerの中でReactのuseを使用する
ペアレントコンポーネント(またはレイアウト)でデータ取得を行い、Promiseを返してから、Reactのuseフックを使用してClient Componentで値をアンラップすることをお勧めします。
Next.jsはサーバー上で早期にデータ取得を開始できます。この例では、それはアプリケーションのエントリポイントであるルートレイアウトです。サーバーはすぐにクライアントへのレスポンスストリーミングを開始できます。
ルートレイアウトにデータ取得を「ホイスト」することで、Next.jsはアプリケーション内の他のコンポーネントより前にサーバー上で指定されたリクエストを開始します。これにより、クライアントのウォーターフォールが排除され、クライアントとサーバー間の複数ラウンドトリップが防止されます。また、サーバーがデータベースに近い(理想的には共存している)ため、パフォーマンスを大幅に改善できます。
例えば、ルートレイアウトを更新してPromiseを呼び出しますが、awaitはしないでください。
import { UserProvider } from './user-provider'
import { getUser } from './user' // サーバー側の関数
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
let userPromise = getUser() // awaitしないこと
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}単一のPromiseを遅延させてClient Componentにプロップとして渡すことができますが、一般的にこのパターンはReactコンテキストプロバイダーと組み合わせられます。これにより、カスタムReact Hookを使用してClient Componentからより簡単にアクセスできます。
Promiseをレスポンスのコンテキストプロバイダーに転送できます。
'use client';
import { createContext, useContext, ReactNode } from 'react';
type User = any;
type UserContextType = {
userPromise: Promise<User | null>;
};
const UserContext = createContext<UserContextType | null>(null);
export function useUser(): UserContextType {
let context = useContext(UserContext);
if (context === null) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
export function UserProvider({
children,
userPromise
}: {
children: ReactNode;
userPromise: Promise<User | null>;
}) {
return (
<UserContext.Provider value={{ userPromise }}>
{children}
</UserContext.Provider>
);
}最後に、任意のClient ComponentでuseUser()カスタムフックを呼び出し、Promiseをアンラップできます。
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}Promiseを消費するコンポーネント(例えば上記のProfile)はサスペンドされます。これにより、部分的なハイドレーションが可能になります。JavaScriptの読み込みが完了する前に、ストリーミングされ事前レンダリングされたHTMLを確認できます。
SWRを使用するSPA
SWRはデータ取得用の人気のあるReactライブラリです。
SWR 2.3.0(およびReact 19+)では、既存のSWRベースのクライアントデータ取得コードとともにサーバー機能を段階的に導入できます。これは上記のuse()パターンの抽象化です。つまり、データ取得をクライアントとサーバー側の間で移動させたり、両方を使用したりできます。
- クライアントのみ:
useSWR(key, fetcher) - サーバーのみ:
useSWR(key)+ RSCが提供するデータ - 混合:
useSWR(key, fetcher)+ RSCが提供するデータ
例えば、<SWRConfig>とfallbackでアプリケーションをラップします。
import { SWRConfig } from 'swr'
import { getUser } from './user' // サーバー側の関数
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SWRConfig
value={{
fallback: {
// ここではgetUser()をawaitしません
// このデータを読み取るコンポーネントのみがサスペンドされます
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}これはServer Componentであるため、getUser()はクッキーやヘッダーを安全に読むか、データベースと通信できます。別のAPIルートは必要ありません。<SWRConfig>の下のClient Componentは、同じキーでuseSWR()を呼び出してユーザーデータを取得できます。useSWRを含むコンポーネントコードは、既存のクライアント取得ソリューションから一切変更する必要がありません。
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// すでに知っている同じSWRパターン
const { data, error } = useSWR('/api/user', fetcher)
return '...'
}fallbackデータは事前レンダリングされ、初期HTMLレスポンスに含まれ、その後useSWRを使用して子コンポーネントで即座に読み取られます。SWRのポーリング、再検証、キャッシュはクライアント側のみで実行されるため、SPAに依存するすべてのインタラクティビティが保持されます。
初期fallbackデータはNext.jsによって自動的に処理されるため、dataがundefinedであるかをチェックするために以前必要だった条件ロジックを削除できるようになりました。データがロード中の場合、最も近い<Suspense>の境界がサスペンドされます。
| SWR | RSC | RSC + SWR | |
|---|---|---|---|
| SSRデータ | |||
| SSR中のストリーミング | |||
| リクエスト重複排除 | |||
| クライアント側機能 |
React Queryを使用するSPA
React QueryをNext.jsとともにクライアント側とサーバー側の両方で使用できます。これにより、厳密なSPAを構築しながら、React QueryとペアになったNext.jsのサーバー機能を活用できます。
詳細はReact Queryドキュメントをご覧ください。
ブラウザのみでコンポーネントをレンダリングする
Client Componentはnext build時に事前レンダリングされます。Client Componentの事前レンダリングを無効にして、ブラウザ環境のみでロードしたい場合は、next/dynamicを使用できます。
import dynamic from 'next/dynamic'
const ClientOnlyComponent = dynamic(() => import('./component'), {
ssr: false,
})これはwindowやdocumentなどのブラウザAPIに依存するサードパーティライブラリに便利です。これらのAPIの存在をチェックするuseEffectを追加し、存在しない場合はnullまたはロード状態を返すこともできます。これは事前レンダリングされます。
クライアント側のシャローワーティング
Create React AppやViteのような厳密なSPAから移行する場合、URL状態を更新するためのシャローワーティングコードがある可能性があります。これは、デフォルトのNext.jsファイルシステムルーティングを使用せずにアプリケーション内のビュー間の手動遷移に便利です。
Next.jsはネイティブのwindow.history.pushStateとwindow.history.replaceStateメソッドを使用して、ページをリロードせずにブラウザの履歴スタックを更新できます。
pushStateとreplaceStateの呼び出しはNext.js Routerに統合され、usePathnameとuseSearchParamsと同期できます。
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const urlSearchParams = new URLSearchParams(searchParams.toString())
urlSearchParams.set('sort', sortOrder)
window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>昇順でソート</button>
<button onClick={() => updateSorting('desc')}>降順でソート</button>
</>
)
}'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder) {
const urlSearchParams = new URLSearchParams(searchParams.toString())
urlSearchParams.set('sort', sortOrder)
window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>昇順でソート</button>
<button onClick={() => updateSorting('desc')}>降順でソート</button>
</>
)
}Next.jsのルーティングとナビゲーションがどのように機能するかについて詳しく学んでください。
Client ComponentでServer Actionsを使用する
Client Componentを使用しながら、Server Actionsを段階的に導入できます。これにより、APIルートを呼び出すためのボイラープレートコードを削除し、useActionStateなどのReact機能を使用してロードと エラー状態を処理できます。
例えば、最初のServer Actionを作成します。
'use server'
export async function create() {}JavaScriptの関数を呼び出すのと同様にクライアントからServer Actionをインポートして使用できます。APIエンドポイントを手動で作成する必要はありません。
'use client'
import { create } from './actions'
export function Button() {
return <button onClick={() => create()}>作成</button>
}Server Actionsでデータを変更する方法について詳しく学んでください。
静的エクスポート(オプション)
Next.jsは完全に静的サイトを生成することもサポートしています。これは厳密なSPAよりもいくつかの利点があります。
- 自動コード分割:単一の
index.htmlを配信する代わりに、Next.jsはルートごとにHTMLファイルを生成するため、ユーザーはクライアントJavaScriptバンドルを待つことなくコンテンツをより速く取得できます。 - ユーザーエクスペリエンスの向上:すべてのルートに対して最小限のスケルトンの代わりに、各ルート用に完全にレンダリングされたページを取得します。ユーザーがクライアント側でナビゲートする場合、トランジションは即座でSPAのようなままです。
静的エクスポートを有効にするには、設定を更新します。
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'export',
}
export default nextConfignext buildを実行した後、Next.jsはアプリケーション用のHTML/CSS/JSアセットを含むoutフォルダを作成します。
注意:静的エクスポートではNext.jsサーバー機能がサポートされていません。詳細を学んでください。
既存プロジェクトをNext.jsに移行する
以下のガイドに従うことで、Next.jsへの段階的な移行ができます。
Pages RouterでSPAを既に使用している場合、段階的にApp Routerを導入する方法を学べます。