Next.jsでシングルページアプリケーションを構築する方法
Next.jsはシングルページアプリケーション(SPA)の構築を完全にサポートしています。
これには、プリフェッチによる高速なルート遷移、クライアントサイドのデータフェッチング、ブラウザAPIの使用、サードパーティのクライアントライブラリとの統合、静的ルートの作成などが含まれます。
既存のSPAをお持ちの場合、コードに大きな変更を加えることなくNext.jsに移行できます。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として始めることができます。プロジェクトが成長すれば、必要に応じて段階的により多くのサーバー機能(例:React Server Components、Server Actionsなど)を追加できます。
例
SPAの構築に使用される一般的なパターンとNext.jsによる解決方法を見ていきましょう。
コンテキストプロバイダー内でReactのuse
を使用する
親コンポーネント(またはレイアウト)でデータをフェッチし、Promiseを返し、そしてReactのuse
フックを用いてクライアントコンポーネントで値を展開することをお勧めします。
Next.jsはサーバー上で早期にデータフェッチングを開始できます。この例ではルートレイアウト(アプリケーションのエントリーポイント)がそれに当たります。サーバーはクライアントへの応答のストリーミングをすぐに開始できます。
データフェッチングをルートレイアウトに「巻き上げる」ことで、Next.jsはアプリケーションの他のコンポーネントよりも先にサーバー上で指定されたリクエストを開始します。これによりクライアントのウォーターフォールが排除され、クライアントとサーバー間の複数の往復が防止されます。また、サーバーがデータベースの場所に近い(理想的には同じ場所にある)ため、パフォーマンスが大幅に向上する可能性があります。
例えば、ルートレイアウトを更新してPromiseを呼び出しますが、awaitは使用しません。
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を遅延させてクライアントコンポーネントにプロップとして渡すことも可能ですが、一般的にはこのパターンはReactコンテキストプロバイダーと組み合わせて使用されます。これにより、カスタムReactフックを使用してクライアントコンポーネントからのアクセスが容易になります。
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>
);
}
最後に、任意のクライアントコンポーネントで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' // some server-side function
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SWRConfig
value={{
fallback: {
// We do NOT await getUser() here
// Only components that read this data will suspend
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}
これはサーバーコンポーネントなので、getUser()
はクッキー、ヘッダーを安全に読み取ったり、データベースと通信したりできます。別のAPIルートは必要ありません。<SWRConfig>
の下のクライアントコンポーネントは、同じキーでuseSWR()
を呼び出してユーザーデータを取得できます。useSWR
を使用するコンポーネントコードは既存のクライアントフェッチングソリューションから変更を必要としません。
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// The same SWR pattern you already know
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を構築できるだけでなく、Next.jsのサーバー機能をReact Queryと組み合わせて活用することもできます。
詳細はReact Queryのドキュメントを参照してください。
コンポーネントをブラウザでのみレンダリングする
クライアントコンポーネントはnext build
中にプリレンダリングされます。クライアントコンポーネントのプリレンダリングを無効にし、ブラウザ環境でのみ読み込む場合は、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ルーターと統合されており、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')}>Sort Ascending</button>
<button onClick={() => updateSorting('desc')}>Sort Descending</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')}>Sort Ascending</button>
<button onClick={() => updateSorting('desc')}>Sort Descending</button>
</>
)
}
Next.jsでのルーティングとナビゲーションの仕組みについて詳しく学びましょう。
クライアントコンポーネントでのServer Actionsの使用
クライアントコンポーネントを使用しながらも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()}>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 nextConfig
next build
を実行すると、Next.jsはアプリケーション用のHTML/CSS/JSアセットを含むout
フォルダを作成します。
注意: 静的エクスポートではNext.jsのサーバー機能はサポートされていません。詳細はこちら。
既存のプロジェクトをNext.jsに移行する
以下のガイドに従って段階的にNext.jsに移行できます:
すでにPages RouterでSPAを使用している場合は、App Routerを段階的に採用する方法について学ぶことができます。