Einführung/Anleitungen/SPAs

Einzelseitenanwendungen (SPAs) mit Next.js erstellen

Next.js unterstützt die Erstellung von Einzelseitenanwendungen (Single-Page Applications, SPAs) vollständig.

Dazu gehören schnelle Routenübergänge mit Prefetching, clientseitiges Datenabrufen, die Verwendung von Browser-APIs, die Integration mit Drittanbieter-Client-Bibliotheken, das Erstellen statischer Routen und mehr.

Wenn Sie bereits eine SPA haben, können Sie zu Next.js migrieren, ohne Ihren Code wesentlich ändern zu müssen. Next.js ermöglicht es Ihnen dann, schrittweise Server-Funktionen nach Bedarf hinzuzufügen.

Was ist eine Einzelseitenanwendung?

Die Definition einer SPA variiert. Wir definieren eine "strikte SPA" als:

  • Client-seitiges Rendering (CSR): Die Anwendung wird durch eine HTML-Datei (z.B. index.html) bereitgestellt. Jede Route, Seitenübergang und Datenabfrage wird durch JavaScript im Browser behandelt.
  • Keine vollständigen Seitenneuladungen: Anstatt ein neues Dokument für jede Route anzufordern, manipuliert clientseitiges JavaScript das DOM der aktuellen Seite und ruft Daten nach Bedarf ab.

Strikte SPAs benötigen oft große Mengen an JavaScript, die geladen werden müssen, bevor die Seite interaktiv sein kann. Zudem können clientseitige Daten-Wasserfälle schwer zu verwalten sein. Die Erstellung von SPAs mit Next.js kann diese Probleme lösen.

Warum Next.js für SPAs verwenden?

Next.js kann Ihre JavaScript-Bundles automatisch code-splitten und mehrere HTML-Einstiegspunkte für verschiedene Routen generieren. Dadurch wird vermieden, dass unnötiger JavaScript-Code clientseitig geladen wird, was die Bundle-Größe reduziert und schnellere Seitenladezeiten ermöglicht.

Die next/link-Komponente prefetched automatisch Routen, was Ihnen die schnellen Seitenübergänge einer strikten SPA bietet, aber mit dem Vorteil, dass der Anwendungs-Routing-Zustand in der URL für Verlinkungen und Sharing erhalten bleibt.

Next.js kann als statische Website oder sogar als strikte SPA starten, bei der alles clientseitig gerendert wird. Wenn Ihr Projekt wächst, ermöglicht Next.js Ihnen, schrittweise weitere Server-Funktionen (z.B. React Server Components, Server Actions und mehr) nach Bedarf hinzuzufügen.

Beispiele

Lassen Sie uns gängige Muster für die Erstellung von SPAs und deren Lösung mit Next.js untersuchen.

Verwendung von Reacts use innerhalb eines Context Providers

Wir empfehlen, Daten in einer übergeordneten Komponente (oder einem Layout) abzurufen, das Promise zurückzugeben und dann den Wert in einer Client-Komponente mit Reacts use-Hook zu entpacken.

Next.js kann das Datenabrufen frühzeitig auf dem Server starten. In diesem Beispiel ist das das Root-Layout – der Einstiegspunkt Ihrer Anwendung. Der Server kann sofort mit dem Streamen einer Antwort an den Client beginnen.

Indem Sie Ihr Datenabrufen in das Root-Layout "hochziehen", startet Next.js die angegebenen Anfragen auf dem Server frühzeitig, bevor andere Komponenten Ihrer Anwendung geladen werden. Dies eliminiert clientseitige Wasserfälle und verhindert mehrere Roundtrips zwischen Client und Server. Es kann auch die Leistung erheblich verbessern, da Ihr Server näher (und idealerweise colokalisiert) an Ihrer Datenbank ist.

Aktualisieren Sie beispielsweise Ihr Root-Layout, um das Promise aufzurufen, aber nicht darauf zu warten.

import { UserProvider } from './user-provider'
import { getUser } from './user' // eine serverseitige Funktion

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  let userPromise = getUser() // NICHT await verwenden

  return (
    <html lang="en">
      <body>
        <UserProvider userPromise={userPromise}>{children}</UserProvider>
      </body>
    </html>
  )
}
import { UserProvider } from './user-provider'
import { getUser } from './user' // eine serverseitige Funktion

export default function RootLayout({ children }) {
  let userPromise = getUser() // NICHT await verwenden

  return (
    <html lang="en">
      <body>
        <UserProvider userPromise={userPromise}>{children}</UserProvider>
      </body>
    </html>
  )
}

Während Sie ein einzelnes Promise verzögern und als Prop an eine Client-Komponente übergeben können, sehen wir dieses Muster häufig in Kombination mit einem React Context Provider. Dies ermöglicht einen einfacheren Zugriff von Client-Komponenten mit einem benutzerdefinierten React-Hook.

Sie können ein Promise an den React Context Provider weiterleiten:

'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>
  );
}
'use client'

import { createContext, useContext, ReactNode } from 'react'

const UserContext = createContext(null)

export function useUser() {
  let context = useContext(UserContext)
  if (context === null) {
    throw new Error('useUser must be used within a UserProvider')
  }
  return context
}

export function UserProvider({ children, userPromise }) {
  return (
    <UserContext.Provider value={{ userPromise }}>
      {children}
    </UserContext.Provider>
  )
}

Schließlich können Sie den useUser()-Hook in jeder Client-Komponente aufrufen und das Promise entpacken:

'use client'

import { use } from 'react'
import { useUser } from './user-provider'

export function Profile() {
  const { userPromise } = useUser()
  const user = use(userPromise)

  return '...'
}
'use client'

import { use } from 'react'
import { useUser } from './user-provider'

export function Profile() {
  const { userPromise } = useUser()
  const user = use(userPromise)

  return '...'
}

Die Komponente, die das Promise konsumiert (z.B. Profile oben), wird suspendiert. Dies ermöglicht partielle Hydration. Sie können das gestreamte und vorgerenderte HTML sehen, bevor JavaScript vollständig geladen ist.

SPAs mit SWR

SWR ist eine beliebte React-Bibliothek für das Datenabrufen.

Mit SWR 2.3.0 (und React 19+) können Sie Server-Funktionen schrittweise neben Ihrem bestehenden SWR-basierten clientseitigen Datenabrufcode übernehmen. Dies ist eine Abstraktion des obigen use()-Musters. Das bedeutet, Sie können Datenabruf zwischen Client und Server verschieben oder beides verwenden:

  • Nur Client: useSWR(key, fetcher)
  • Nur Server: useSWR(key) + RSC-bereitgestellte Daten
  • Gemischt: useSWR(key, fetcher) + RSC-bereitgestellte Daten

Beispielsweise können Sie Ihre Anwendung mit <SWRConfig> und einem fallback umschließen:

import { SWRConfig } from 'swr'
import { getUser } from './user' // eine serverseitige Funktion

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // Wir verwenden hier KEIN await für getUser()
          // Nur Komponenten, die diese Daten lesen, werden suspendiert
          '/api/user': getUser(),
        },
      }}
    >
      {children}
    </SWRConfig>
  )
}
import { SWRConfig } from 'swr'
import { getUser } from './user' // eine serverseitige Funktion

export default function RootLayout({ children }) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // Wir verwenden hier KEIN await für getUser()
          // Nur Komponenten, die diese Daten lesen, werden suspendiert
          '/api/user': getUser(),
        },
      }}
    >
      {children}
    </SWRConfig>
  )
}

Da dies eine Server-Komponente ist, kann getUser() sicher Cookies, Header lesen oder mit Ihrer Datenbank kommunizieren. Es ist keine separate API-Route erforderlich. Client-Komponenten unterhalb von <SWRConfig> können useSWR() mit demselben Schlüssel aufrufen, um die Benutzerdaten abzurufen. Der Komponentencode mit useSWR erfordert keine Änderungen gegenüber Ihrer bestehenden clientseitigen Abfragelösung.

'use client'

import useSWR from 'swr'

export function Profile() {
  const fetcher = (url) => fetch(url).then((res) => res.json())
  // Das gleiche SWR-Muster, das Sie bereits kennen
  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())
  // Das gleiche SWR-Muster, das Sie bereits kennen
  const { data, error } = useSWR('/api/user', fetcher)

  return '...'
}

Die fallback-Daten können vorgerendert und in der initialen HTML-Antwort enthalten sein, dann sofort in den untergeordneten Komponenten mit useSWR gelesen werden. SWRs Polling, Revalidierung und Caching laufen weiterhin nur clientseitig, wodurch die gesamte Interaktivität erhalten bleibt, die Sie für eine SPA benötigen.

Da die initialen fallback-Daten automatisch von Next.js behandelt werden, können Sie jetzt jegliche bedingte Logik löschen, die zuvor benötigt wurde, um zu prüfen, ob data undefined war. Während die Daten geladen werden, wird die nächstgelegene <Suspense>-Grenze suspendiert.

SWRRSCRSC + SWR
SSR-DatenCross IconCheck IconCheck Icon
Streaming während SSRCross IconCheck IconCheck Icon
Anfragen deduplizierenCheck IconCheck IconCheck Icon
Client-seitige FunktionenCheck IconCross IconCheck Icon

SPAs mit React Query

Sie können React Query mit Next.js sowohl auf dem Client als auch auf dem Server verwenden. Dies ermöglicht es Ihnen, sowohl strikte SPAs zu erstellen als auch Server-Funktionen in Next.js in Kombination mit React Query zu nutzen.

Erfahren Sie mehr in der React Query-Dokumentation.

Rendern von Komponenten nur im Browser

Client-Komponenten werden während next build vorgerendert. Wenn Sie das Vorrendering für eine Client-Komponente deaktivieren und sie nur in der Browser-Umgebung laden möchten, können Sie next/dynamic verwenden:

import dynamic from 'next/dynamic'

const ClientOnlyComponent = dynamic(() => import('./component'), {
  ssr: false,
})

Dies kann nützlich sein für Drittanbieter-Bibliotheken, die auf Browser-APIs wie window oder document angewiesen sind. Sie können auch einen useEffect hinzufügen, der die Existenz dieser APIs überprüft, und wenn sie nicht vorhanden sind, null oder einen Ladezustand zurückgeben, der vorgerendert wird.

Shallow Routing auf dem Client

Wenn Sie von einer strikten SPA wie Create React App oder Vite migrieren, haben Sie möglicherweise bestehenden Code, der shallow routing verwendet, um den URL-Zustand zu aktualisieren. Dies kann nützlich sein für manuelle Übergänge zwischen Ansichten in Ihrer Anwendung ohne die standardmäßige Next.js-Dateisystem-Routing zu verwenden.

Next.js ermöglicht es Ihnen, die nativen Methoden window.history.pushState und window.history.replaceState zu verwenden, um den Browser-Verlauf zu aktualisieren, ohne die Seite neu zu laden.

pushState- und replaceState-Aufrufe integrieren sich in den Next.js-Router, sodass Sie mit usePathname und useSearchParams synchronisiert werden können.

'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')}>Aufsteigend sortieren</button>
      <button onClick={() => updateSorting('desc')}>Absteigend sortieren</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')}>Aufsteigend sortieren</button>
      <button onClick={() => updateSorting('desc')}>Absteigend sortieren</button>
    </>
  )
}

Erfahren Sie mehr darüber, wie Routing und Navigation in Next.js funktionieren.

Verwendung von Server Actions in Client-Komponenten

Sie können Server Actions schrittweise übernehmen, während Sie weiterhin Client-Komponenten verwenden. Dadurch können Sie Boilerplate-Code zum Aufrufen einer API-Route entfernen und stattdessen React-Funktionen wie useActionState verwenden, um Lade- und Fehlerzustände zu behandeln.

Erstellen Sie beispielsweise Ihre erste Server Action:

'use server'

export async function create() {}
'use server'

export async function create() {}

Sie können eine Server Action vom Client importieren und verwenden, ähnlich wie das Aufrufen einer JavaScript-Funktion. Sie müssen keine API-Endpoint manuell erstellen:

'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Erstellen</button>
}
'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Erstellen</button>
}

Erfahren Sie mehr über Datenmutation mit Server Actions.

Statischer Export (optional)

Next.js unterstützt auch die Generierung einer vollständig statischen Website. Dies hat einige Vorteile gegenüber strikten SPAs:

  • Automatisches Code-Splitting: Anstatt ein einzelnes index.html auszuliefern, generiert Next.js eine HTML-Datei pro Route, sodass Ihre Besucher den Inhalt schneller erhalten, ohne auf das clientseitige JavaScript-Bundle warten zu müssen.
  • Verbessertes Nutzererlebnis: Anstatt eines minimalen Skeletts für alle Routen erhalten Sie vollständig gerenderte Seiten für jede Route. Wenn Benutzer clientseitig navigieren, bleiben die Übergänge instant und SPA-ähnlich.

Um einen statischen Export zu aktivieren, aktualisieren Sie Ihre Konfiguration:

next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export',
}

export default nextConfig

Nach Ausführung von next build erstellt Next.js einen out-Ordner mit den HTML/CSS/JS-Assets für Ihre Anwendung.

Hinweis: Next.js-Server-Funktionen werden mit statischen Exporten nicht unterstützt. Erfahren Sie mehr.

Migration bestehender Projekte zu Next.js

Sie können schrittweise zu Next.js migrieren, indem Sie unseren Anleitungen folgen:

Wenn Sie bereits eine SPA mit dem Pages Router verwenden, können Sie lernen, wie Sie den App Router schrittweise übernehmen.