コンテンツにスキップ
アプリケーションの構築アップグレードシングルページアプリケーション

Next.jsにおけるシングルページアプリケーション

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

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

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

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

SPAの定義は様々です。「厳密なSPA」は次のように定義します。

  • クライアントサイドレンダリング (CSR): アプリケーションは単一の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 ComponentsServer Actionsなど)を段階的に追加することを可能にします。

SPAを構築するために使用される一般的なパターンと、Next.jsがそれらをどのように解決するかを見ていきましょう。

Context Provider内でReactのuseを使用する

親コンポーネント(またはレイアウト)でデータをフェッチし、Promiseを返し、その値をReactのuseフックを使用してクライアントコンポーネントでアンラップすることをお勧めします。

Next.jsはサーバー上で早期にデータフェッチを開始できます。この例では、それはアプリケーションのエントリポイントであるルートレイアウトです。サーバーはすぐにクライアントへのレスポンスのストリーミングを開始できます。

データフェッチをルートレイアウトに「持ち上げる」ことで、Next.jsはアプリケーション内の他のコンポーネントよりも早くサーバー上で指定されたリクエストを開始します。これにより、クライアントのウォーターフォールがなくなり、クライアントとサーバー間の複数回のラウンドトリップを防ぎます。また、サーバーがデータベースの近く(理想的には併置)にあるため、パフォーマンスも大幅に向上させることができます。

例えば、ルートレイアウトを更新してPromiseを呼び出しますが、await はしません。

app/layout.tsx
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をReactコンテキストプロバイダーに転送できます。

app/user-provider.ts
'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をアンラップできます。

app/profile.tsx
'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 でラップします。

app/layout.tsx
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 を使用するコンポーネントコードは、既存のクライアントフェッチソリューションから変更を必要としません

app/profile.tsx
'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によって自動的に処理されるため、以前 dataundefined であるかどうかを確認するために必要だった条件ロジックを削除できます。データがロード中の場合、最も近い <Suspense> 境界がサスペンドされます。

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

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

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

Create React AppViteのような厳密な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')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}

Next.jsでのルーティングとナビゲーションの動作について詳しくはこちら。

クライアントコンポーネントでサーバーアクションを使用する

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

例えば、最初のサーバーアクションを作成します。

app/actions.ts
'use server'
 
export async function create() {}

JavaScript関数を呼び出すのと同様に、クライアントからサーバーアクションをインポートして使用できます。APIエンドポイントを手動で作成する必要はありません。

app/button.tsx
'use client'
 
import { create } from './actions'
 
export function Button() {
  return <button onClick={() => create()}>Create</button>
}

サーバーアクションによるデータ変更について詳しくはこちら。

静的エクスポート(オプション)

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を段階的に採用する方法を学ぶことができます。