はじめに/ガイド/SPA

Next.jsでシングルページアプリケーション (SPA) を構築する方法

Next.jsはシングルページアプリケーション (SPA) の構築を完全にサポートしています。

これには、プリフェッチによる高速なルート遷移、クライアントサイドデータフェッチ、ブラウザAPIの使用、サードパーティクライアントライブラリとの統合、静的ルートの作成などが含まれます。

既存のSPAがある場合、コードに大きな変更を加えることなくNext.jsに移行できます。Next.jsでは、必要に応じてサーバー機能を段階的に追加することが可能です。

シングルページアプリケーション (SPA) とは?

SPAの定義は様々です。ここでは「厳密なSPA」を以下のように定義します:

  • クライアントサイドレンダリング (CSR): アプリは1つのHTMLファイル(例: index.html)で提供されます。すべてのルート、ページ遷移、データフェッチはブラウザ上のJavaScriptによって処理されます。
  • フルページリロードなし: 各ルートに対して新しいドキュメントを要求する代わりに、クライアントサイドJavaScriptが現在のページのDOMを操作し、必要に応じてデータをフェッチします。

厳密なSPAでは、ページがインタラクティブになる前に大量のJavaScriptを読み込む必要がある場合が多く、クライアントデータのウォーターフォール管理も課題になりがちです。Next.jsでSPAを構築することでこれらの問題に対処できます。

Next.jsでSPAを構築する理由

Next.jsはJavaScriptバンドルの自動コード分割が可能で、異なるルートへの複数のHTMLエントリーポイントを生成します。これにより、クライアントサイドで不要なJavaScriptコードを読み込む必要がなくなり、バンドルサイズが縮小され、ページ読み込みが高速化されます。

next/link コンポーネントはルートを自動的にプリフェッチするため、厳密なSPAのような高速なページ遷移を実現しつつ、アプリケーションのルーティング状態をURLに保持してリンクや共有が可能です。

Next.jsは静的サイトや、すべてがクライアントサイドでレンダリングされる厳密なSPAとして始めることができます。プロジェクトが成長した場合、必要に応じてReact Server ComponentsServer Actionsなどのサーバー機能を段階的に追加できます。

SPA構築でよく使われるパターンと、Next.jsでの解決方法を見ていきましょう。

Context Provider内でのReactのuse使用

親コンポーネント(またはレイアウト)でデータをフェッチし、Promiseを返してから、Reactのuseフックでクライアントコンポーネント内で値をアンラップすることを推奨します。

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を遅延して渡すことはできますが、一般的にこのパターンはReactコンテキストプロバイダーと組み合わせて使用されます。これにより、カスタムReactフックを使用してクライアントコンポーネントから簡単にアクセスできます。

PromiseをReactコンテキストプロバイダーに転送できます:

'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' // サーバーサイド関数

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // ここではgetUser()をawaitしない
          // このデータを読み取るコンポーネントのみがサスペンドされる
          '/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())
  // 既知のSWRパターン
  const { data, error } = useSWR('/api/user', fetcher)

  return '...'
}

fallbackデータはプリレンダリングされ、初期HTMLレスポンスに含まれ、useSWRを使用する子コンポーネントですぐに読み取られます。SWRのポーリング、再検証、キャッシュは引き続きクライアントサイドのみで実行されるため、SPAに必要なすべてのインタラクティビティが維持されます。

初期のfallbackデータはNext.jsによって自動的に処理されるため、以前はdataundefinedかどうかをチェックするために必要な条件付きロジックを削除できます。データが読み込み中の場合、最も近い<Suspense>境界がサスペンドされます。

SWRRSCRSC + SWR
SSRデータCross IconCheck IconCheck Icon
SSR中のストリーミングCross IconCheck IconCheck Icon
リクエストの重複排除Check IconCheck IconCheck Icon
クライアント機能Check IconCross IconCheck Icon

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,
})

これはwindowdocumentなどのブラウザAPIに依存するサードパーティライブラリに便利です。また、これらのAPIの存在をチェックするuseEffectを追加し、存在しない場合はプリレンダリングされるnullやローディング状態を返すこともできます。

クライアントでのシャロールーティング

Create React AppViteなどの厳密なSPAから移行する場合、URL状態を更新するためにシャロールーティングする既存のコードがあるかもしれません。これは、デフォルトのNext.jsファイルシステムルーティングを使用せずに、アプリケーション内のビュー間で手動遷移を行うのに便利です。

Next.jsでは、ネイティブのwindow.history.pushStatewindow.history.replaceStateメソッドを使用して、ページをリロードせずにブラウザの履歴スタックを更新できます。

pushStatereplaceStateの呼び出しはNext.jsルーターと統合され、usePathnameuseSearchParamsとの同期が可能です。

'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でのルーティングとナビゲーションの仕組みについて詳しく学べます。

クライアントコンポーネントでのServer Actionsの使用

クライアントコンポーネントを使用しながらも、Server Actionsを段階的に採用できます。これにより、APIルートを呼び出すためのボイラープレートコードを削除し、代わりにuseActionStateなどのReact機能を使用してローディングやエラー状態を処理できます。

例えば、最初のServer Actionを作成します:

'use server'

export async function create() {}

クライアントからServer Actionをインポートして使用でき、JavaScript関数を呼び出すのと同様です。手動で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のようなままです。

静的エクスポートを有効にするには、設定を更新します:

next.config.ts
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の段階的採用方法を学べます。