Single-Page Applications with 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として開始することができます。プロジェクトが成長すれば、Next.jsでは必要に応じてReact Server ComponentsやServer Actionsなどのサーバー機能を段階的に追加できます。
例
SPAの構築に使用される一般的なパターンとNext.jsがどのように解決するかを見ていきましょう。
Contextプロバイダー内で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を遅延させてクライアントコンポーネントにpropsとして渡すことも可能ですが、一般的にこのパターンはReactコンテキストプロバイダーと組み合わせて使用されることが多いでしょう。これにより、カスタム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
Next.jsではクライアントとサーバーの両方でReact Queryを使用できます。これにより、厳密なSPAを構築するだけでなく、React QueryとペアになったNext.jsのサーバー機能を活用することもできます。
詳細は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を段階的に採用する方法について学ぶことができます。