Next.jsによるシングルページアプリケーション
Next.jsはシングルページアプリケーション(SPA)の構築を完全にサポートしています。
これには、プリフェッチによる高速なルート遷移、クライアントサイドでのデータ取得、ブラウザAPIの使用、サードパーティのクライアントライブラリとの統合、静的ルートの作成などが含まれます。
既存のSPAをお持ちの場合、大きなコード変更をせずにNext.jsに移行できます。その後、必要に応じてサーバー機能を段階的に追加することができます。
シングルページアプリケーションとは?
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として開始できます。プロジェクトが成長するにつれて、Next.jsは必要に応じてより多くのサーバー機能(例:React Server Components、Server Actionsなど)を段階的に追加することができます。
例
SPAを構築するために使用される一般的なパターンと、それをNext.jsがどのように解決するかを見てみましょう。
Context Provider内でのReactのuse
の使用
親コンポーネント(またはレイアウト)でデータを取得し、Promiseを返し、クライアントコンポーネントでReactのuse
フックを使用して値を展開することをお勧めします。
Next.jsはサーバーで早期にデータ取得を開始できます。この例では、アプリケーションのエントリーポイントであるroot レイアウトです。サーバーはクライアントに対してすぐにレスポンスをストリーミングし始めることができます。
データ取得をroot レイアウトに「持ち上げる」ことで、Next.jsはアプリケーション内の他のコンポーネントよりも早くサーバーで指定されたリクエストを開始します。これによりクライアントのウォーターフォールが排除され、クライアントとサーバー間の複数の往復を防ぎます。また、サーバーがデータベースの近く(理想的には同じ場所)にあるため、パフォーマンスが大幅に向上する可能性があります。
例えば、root レイアウトを更新してPromiseを呼び出しますが、awaitしないでください。
- TypeScript
- JavaScript
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>
)
}
import { UserProvider } from './user-provider'
import { getUser } from './user' // サーバーサイドの関数
export default function RootLayout({ children }) {
let userPromise = getUser() // awaitしない
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
Promiseをクライアントコンポーネントへのpropとして遅延して渡すこともできますが、一般的にこのパターンはReactのコンテキストプロバイダーと組み合わせて使用されます。これにより、カスタムReactフックを使用してクライアントコンポーネントからのアクセスが容易になります。
PromiseをReactのコンテキストプロバイダーに転送できます:
- TypeScript
- JavaScript
'use client';
import { use, createContext, ReactNode } from 'react';
type User = any;
type UserContextType = {
user: User;
};
const UserContext = createContext<UserContextType | null>(null);
export function UserProvider({
children,
userPromise,
}: {
children: ReactNode;
userPromise: Promise<User | null>;
}) {
let initialUser = use(userPromise);
return (
<UserContext.Provider value={{ user: initialUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser(): UserContextType {
let context = use(UserContext);
if (context === null) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
'use client'
import { use, createContext } from 'react'
const UserContext = createContext(null)
export function UserProvider({ children, userPromise }) {
let initialUser = use(userPromise)
return (
<UserContext.Provider value={{ user: initialUser }}>
{children}
</UserContext.Provider>
)
}
export function useUser() {
let context = use(UserContext)
if (context === null) {
throw new Error('useUser must be used within a UserProvider')
}
return context
}
最後に、任意のクライアントコンポーネントでuseUser()
カスタムフックを呼び出すことができます:
- TypeScript
- JavaScript
'use client'
import { useUser } from './user-provider'
export function Profile() {
const { user } = useUser()
return '...'
}
'use client'
import { useUser } from './user-provider'
export function Profile() {
const { user } = useUser()
return '...'
}
Promiseを消費するコンポーネント(上記のProfile
など)はサスペンドされます。これにより部分的なハイドレーションが可能になります。JavaScriptの読み込みが完了する前にストリーミングされたHTMLとプリレンダリングされた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
でアプリケーションをラップします:
- TypeScript
- JavaScript
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>
)
}
import { SWRConfig } from 'swr'
import { getUser } from './user' // サーバーサイドの関数
export default function RootLayout({ children }) {
return (
<SWRConfig
value={{
fallback: {
// ここでgetUser()をawaitしない
// このデータを読むコンポーネントのみがサスペンドされる
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}
これはサーバーコンポーネントであるため、getUser()
はcookie、ヘッダーを安全に読み取ったり、データベースと通信したりできます。別のAPIルートは必要ありません。<SWRConfig>
の下のクライアントコンポーネントは、同じキーでuseSWR()
を呼び出してユーザーデータを取得できます。useSWR
を使用したコンポーネントコードは、既存のクライアント取得ソリューションから変更を必要としません。
- TypeScript
- JavaScript
'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 '...'
}
'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によって自動的に処理されるため、以前に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 Routerに統合され、usePathname
やuseSearchParams
と同期できます。
- TypeScript
- JavaScript
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const params = new URLSearchParams(searchParams.toString())
params.set('sort', sortOrder)
window.history.pushState(null, '', `?${params.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 params = new URLSearchParams(searchParams.toString())
params.set('sort', sortOrder)
window.history.pushState(null, '', `?${params.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を作成します:
- TypeScript
- JavaScript
'use server'
export async function create() {}
'use server'
export async function create() {}
クライアントからServer ActionをJavaScript関数を呼び出すようにインポートして使用できます。APIエンドポイントを手動で作成する必要はありません:
- TypeScript
- JavaScript
'use client'
import { create } from './actions'
export function Button() {
return <button onClick={() => create()}>Create</button>
}
'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を段階的に採用する方法を学ぶことができます。