Einführung/Anleitungen/SPAs

Einführung in Single-Page Applications mit Next.js

Next.js bietet umfassende Unterstützung für die Entwicklung von Single-Page Applications (SPAs).

Dies umfasst schnelle Routenübergänge mit Prefetching, clientseitiges Datenabrufen, die Nutzung von Browser-APIs, die Integration mit Drittanbieter-Client-Bibliotheken, die Erstellung statischer Routen und mehr.

Falls Sie bereits eine SPA haben, können Sie zu Next.js migrieren, ohne große Änderungen an Ihrem Code vornehmen zu müssen. Next.js ermöglicht es Ihnen dann, schrittweise Server-Funktionen nach Bedarf hinzuzufügen.

Was ist eine Single-Page Application?

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

  • Client-seitiges Rendering (CSR): Die App wird durch eine HTML-Datei (z.B. index.html) bereitgestellt. Jede Route, Seitenübergang und Datenabfrage wird durch JavaScript im Browser verarbeitet.
  • Keine vollständigen Seitenneuladungen: Anstatt für jede Route ein neues Dokument 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 Entwicklung 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 Routing-Zustand der Anwendung 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 zur Entwicklung von SPAs und deren Lösung mit Next.js untersuchen.

Verwendung von Reacts use innerhalb eines Context Providers

Wir empfehlen, Daten in einer Elternkomponente (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 liegt.

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

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

Während Sie ein einzelnes Promise verzögern und als Prop an eine Client-Komponente übergeben können, sehen wir dieses Muster generell 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>
  );
}

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 '...'
}

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 fertig geladen hat.

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 das Datenabrufen 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' // some server-side function

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // Wir erwarten hier NICHT 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 wird keine separate API-Route benötigt. 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 '...'
}

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, auf die Sie für eine SPA angewiesen sind.

Da die initialen fallback-Daten automatisch von Next.js verarbeitet 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 Ihnen, sowohl strikte SPAs zu entwickeln 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 Vorrendern 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 falls sie nicht vorhanden sind, null oder einen Ladezustand zurückgeben, der vorgerendert würde.

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 sie mit usePathname und useSearchParams synchronisieren 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 Components

Sie können Server Actions schrittweise übernehmen, während Sie weiterhin Client Components 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 handhaben.

Erstellen Sie beispielsweise Ihre erste Server Action:

'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 keinen API-Endpunkt manuell erstellen:

'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 Benutzererlebnis: 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 instantan 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 dem Ausführen 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 bei statischen Exports 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:

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