Introduction/Guides/SPAs

Comment construire des applications monopages avec Next.js

Next.js prend entièrement en charge la création d'applications monopages (SPA).

Cela inclut des transitions de route rapides avec préchargement, la récupération de données côté client, l'utilisation d'API navigateur, l'intégration avec des bibliothèques client tierces, la création de routes statiques, et plus encore.

Si vous avez déjà une SPA, vous pouvez migrer vers Next.js sans modifier significativement votre code. Next.js vous permet ensuite d'ajouter progressivement des fonctionnalités serveur si nécessaire.

Qu'est-ce qu'une application monopage ?

La définition d'une SPA varie. Nous définirons une "SPA stricte" comme :

  • Rendu côté client (CSR) : L'application est servie par un seul fichier HTML (par exemple index.html). Chaque route, transition de page et récupération de données est gérée par JavaScript dans le navigateur.
  • Pas de rechargement complet de page : Plutôt que de demander un nouveau document pour chaque route, le JavaScript côté client manipule le DOM de la page actuelle et récupère les données si nécessaire.

Les SPA strictes nécessitent souvent de grandes quantités de JavaScript à charger avant que la page ne soit interactive. De plus, les cascades de données côté client peuvent être difficiles à gérer. Construire des SPA avec Next.js peut résoudre ces problèmes.

Pourquoi utiliser Next.js pour les SPA ?

Next.js peut diviser automatiquement vos bundles JavaScript et générer plusieurs points d'entrée HTML pour différentes routes. Cela évite de charger du code JavaScript inutile côté client, réduisant la taille du bundle et permettant des chargements de page plus rapides.

Le composant next/link précharge automatiquement les routes, vous offrant les transitions rapides d'une SPA stricte, mais avec l'avantage de persister l'état de routage de l'application dans l'URL pour le partage.

Next.js peut commencer comme un site statique ou même une SPA stricte où tout est rendu côté client. Si votre projet évolue, Next.js vous permet d'ajouter progressivement plus de fonctionnalités serveur (par exemple Composants Serveur React, Actions Serveur, etc.) si nécessaire.

Exemples

Explorons les modèles courants utilisés pour construire des SPA et comment Next.js les résout.

Utilisation du hook use de React dans un fournisseur de contexte

Nous recommandons de récupérer les données dans un composant parent (ou une mise en page), de renvoyer la Promise, puis de déballer la valeur dans un Composant Client avec le hook use de React.

Next.js peut commencer la récupération de données tôt sur le serveur. Dans cet exemple, c'est la mise en page racine - le point d'entrée de votre application. Le serveur peut immédiatement commencer à diffuser une réponse au client.

En "haussant" votre récupération de données vers la mise en page racine, Next.js démarre les requêtes spécifiées sur le serveur avant tout autre composant de votre application. Cela élimine les cascades côté client et évite les allers-retours multiples entre client et serveur. Cela peut aussi améliorer significativement les performances, car votre serveur est plus proche (et idéalement colocalisé) de votre base de données.

Par exemple, mettez à jour votre mise en page racine pour appeler la Promise, mais ne l'attendez pas.

import { UserProvider } from './user-provider'
import { getUser } from './user' // une fonction côté serveur

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

  return (
    <html lang="en">
      <body>
        <UserProvider userPromise={userPromise}>{children}</UserProvider>
      </body>
    </html>
  )
}
import { UserProvider } from './user-provider'
import { getUser } from './user' // une fonction côté serveur

export default function RootLayout({ children }) {
  let userPromise = getUser() // ne PAS await

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

Bien que vous puissiez différer et passer une seule Promise comme prop à un Composant Client, nous voyons généralement ce modèle associé à un fournisseur de contexte React. Cela permet un accès plus facile depuis les Composants Clients avec un hook React personnalisé.

Vous pouvez transmettre une Promise au fournisseur de contexte React :

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

Enfin, vous pouvez appeler le hook personnalisé useUser() dans n'importe quel Composant Client et déballer la Promise :

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

Le composant qui consomme la Promise (par exemple Profile ci-dessus) sera suspendu. Cela permet une hydratation partielle. Vous pouvez voir le HTML streamé et prérendu avant que JavaScript n'ait fini de charger.

SPAs avec SWR

SWR est une bibliothèque React populaire pour la récupération de données.

Avec SWR 2.3.0 (et React 19+), vous pouvez adopter progressivement des fonctionnalités serveur aux côtés de votre code existant de récupération de données côté client basé sur SWR. C'est une abstraction du modèle use() ci-dessus. Cela signifie que vous pouvez déplacer la récupération de données entre le client et le serveur, ou utiliser les deux :

  • Client uniquement : useSWR(key, fetcher)
  • Serveur uniquement : useSWR(key) + données fournies par RSC
  • Mixte : useSWR(key, fetcher) + données fournies par RSC

Par exemple, enveloppez votre application avec <SWRConfig> et un fallback :

import { SWRConfig } from 'swr'
import { getUser } from './user' // une fonction côté serveur

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // Nous n'attendons PAS getUser() ici
          // Seuls les composants qui lisent ces données seront suspendus
          '/api/user': getUser(),
        },
      }}
    >
      {children}
    </SWRConfig>
  )
}
import { SWRConfig } from 'swr'
import { getUser } from './user' // une fonction côté serveur

export default function RootLayout({ children }) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // Nous n'attendons PAS getUser() ici
          // Seuls les composants qui lisent ces données seront suspendus
          '/api/user': getUser(),
        },
      }}
    >
      {children}
    </SWRConfig>
  )
}

Comme il s'agit d'un Composant Serveur, getUser() peut lire en toute sécurité les cookies, en-têtes ou parler à votre base de données. Aucune route API séparée n'est nécessaire. Les composants clients sous <SWRConfig> peuvent appeler useSWR() avec la même clé pour récupérer les données utilisateur. Le code du composant avec useSWR ne nécessite aucun changement par rapport à votre solution existante de récupération côté client.

'use client'

import useSWR from 'swr'

export function Profile() {
  const fetcher = (url) => fetch(url).then((res) => res.json())
  // Le même modèle SWR que vous connaissez déjà
  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())
  // Le même modèle SWR que vous connaissez déjà
  const { data, error } = useSWR('/api/user', fetcher)

  return '...'
}

Les données fallback peuvent être prérendues et incluses dans la réponse HTML initiale, puis immédiatement lues dans les composants enfants avec useSWR. Le sondage, la revalidation et la mise en cache de SWR s'exécutent toujours uniquement côté client, préservant ainsi toute l'interactivité dont vous dépendez pour une SPA.

Comme les données fallback initiales sont automatiquement gérées par Next.js, vous pouvez maintenant supprimer toute logique conditionnelle précédemment nécessaire pour vérifier si data était undefined. Lorsque les données sont en cours de chargement, la limite <Suspense> la plus proche sera suspendue.

SWRRSCRSC + SWR
Données SSRCross IconCheck IconCheck Icon
Streaming pendant SSRCross IconCheck IconCheck Icon
Déduplication requêtesCheck IconCheck IconCheck Icon
Fonctionnalités clientCheck IconCross IconCheck Icon

SPAs avec React Query

Vous pouvez utiliser React Query avec Next.js à la fois côté client et serveur. Cela vous permet de construire à la fois des SPA strictes, ainsi que de tirer parti des fonctionnalités serveur de Next.js couplées à React Query.

Apprenez-en plus dans la documentation React Query.

Rendu des composants uniquement dans le navigateur

Les composants clients sont prérendus pendant next build. Si vous souhaitez désactiver le prérendu pour un Composant Client et ne le charger que dans l'environnement du navigateur, vous pouvez utiliser next/dynamic :

import dynamic from 'next/dynamic'

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

Cela peut être utile pour les bibliothèques tierces qui dépendent d'API navigateur comme window ou document. Vous pouvez aussi ajouter un useEffect qui vérifie l'existence de ces API, et si elles n'existent pas, renvoyer null ou un état de chargement qui serait prérendu.

Routage superficiel côté client

Si vous migrez depuis une SPA stricte comme Create React App ou Vite, vous pourriez avoir du code existant qui route superficiellement pour mettre à jour l'état de l'URL. Cela peut être utile pour les transitions manuelles entre les vues de votre application sans utiliser le routage par système de fichiers par défaut de Next.js.

Next.js vous permet d'utiliser les méthodes natives window.history.pushState et window.history.replaceState pour mettre à jour la pile d'historique du navigateur sans recharger la page.

Les appels pushState et replaceState s'intègrent au routeur Next.js, vous permettant de synchroniser avec usePathname et useSearchParams.

'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')}>Trier par ordre croissant</button>
      <button onClick={() => updateSorting('desc')}>Trier par ordre décroissant</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')}>Trier par ordre croissant</button>
      <button onClick={() => updateSorting('desc')}>Trier par ordre décroissant</button>
    </>
  )
}

Apprenez-en plus sur le fonctionnement du routage et de la navigation dans Next.js.

Utilisation d'Actions Serveur dans les Composants Clients

Vous pouvez adopter progressivement les Actions Serveur tout en utilisant des Composants Clients. Cela vous permet de supprimer le code passe-partout pour appeler une route API, et d'utiliser plutôt des fonctionnalités React comme useActionState pour gérer les états de chargement et d'erreur.

Par exemple, créez votre première Action Serveur :

'use server'

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

export async function create() {}

Vous pouvez importer et utiliser une Action Serveur depuis le client, similairement à l'appel d'une fonction JavaScript. Vous n'avez pas besoin de créer manuellement un point de terminaison API :

'use client'

import { create } from './actions'

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

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Créer</button>
}

Apprenez-en plus sur la modification de données avec les Actions Serveur.

Export statique (optionnel)

Next.js prend également en charge la génération d'un site entièrement statique. Cela présente certains avantages par rapport aux SPA strictes :

  • Division de code automatique : Au lieu d'envoyer un seul index.html, Next.js générera un fichier HTML par route, donc vos visiteurs obtiennent le contenu plus rapidement sans attendre le bundle JavaScript client.
  • Expérience utilisateur améliorée : Au lieu d'un squelette minimal pour toutes les routes, vous obtenez des pages entièrement rendues pour chaque route. Lorsque les utilisateurs naviguent côté client, les transitions restent instantanées et similaires à une SPA.

Pour activer un export statique, mettez à jour votre configuration :

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

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

export default nextConfig

Après avoir exécuté next build, Next.js créera un dossier out avec les ressources HTML/CSS/JS de votre application.

Note : Les fonctionnalités serveur de Next.js ne sont pas prises en charge avec les exports statiques. En savoir plus.

Migration de projets existants vers Next.js

Vous pouvez migrer progressivement vers Next.js en suivant nos guides :

Si vous utilisez déjà une SPA avec le Pages Router, vous pouvez apprendre comment adopter progressivement l'App Router.