Sponsor
ChatHubChatHub Use GPT-4, Gemini, Claude 3.5 and more chatbots side-by-side
ここをクリック
Menu

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 ComponentsServer Actionsなどのサーバー機能を段階的に追加できます。

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

Contextプロバイダー内でReactのuseを使用する

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

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

データフェッチングをルートレイアウトに「巻き上げる」ことで、Next.jsはアプリケーション内の他のコンポーネントより前にサーバー上で指定されたリクエストを早期に開始します。これによりクライアントウォーターフォールが排除され、クライアントとサーバー間の複数の往復が防止されます。また、サーバーがデータベースの場所に近い(理想的には同じ場所に配置される)ため、パフォーマンスを大幅に向上させることができます。

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

app/layout.tsx
TypeScript
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を転送できます:

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

最後に、任意のクライアントコンポーネントで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' // 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
TypeScript
'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

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

これは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')}>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を作成します:

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