Menu

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 ComponentsServer Actionsなど)を段階的に追加できます。

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

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

ペアレントコンポーネント(またはレイアウト)でデータ取得を行い、Promiseを返してから、Reactのuseフックを使用してClient Componentで値をアンラップすることをお勧めします。

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

ルートレイアウトにデータ取得を「ホイスト」することで、Next.jsはアプリケーション内の他のコンポーネントより前にサーバー上で指定されたリクエストを開始します。これにより、クライアントのウォーターフォールが排除され、クライアントとサーバー間の複数ラウンドトリップが防止されます。また、サーバーがデータベースに近い(理想的には共存している)ため、パフォーマンスを大幅に改善できます。

例えば、ルートレイアウトを更新してPromiseを呼び出しますが、awaitはしないでください。

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

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

app/profile.tsx
TypeScript
'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
TypeScript
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を含むコンポーネントコードは、既存のクライアント取得ソリューションから一切変更する必要がありません

app/profile.tsx
TypeScript
'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データ
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,
})

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

クライアント側のシャローワーティング

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

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

pushStatereplaceStateの呼び出しはNext.js Routerに統合され、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のルーティングとナビゲーションがどのように機能するかについて詳しく学んでください。

Client ComponentでServer Actionsを使用する

Client Componentを使用しながら、Server Actionsを段階的に導入できます。これにより、APIルートを呼び出すためのボイラープレートコードを削除し、useActionStateなどのReact機能を使用してロードと エラー状態を処理できます。

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

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

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

app/button.tsx
TypeScript
'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を導入する方法を学べます。