Next.js 15 fullstack (App Router + Server Actions) · Lektion

Bygga ett foto-modalt flöde med mjuk navigering

Kombinera interception och parallella routes för att öppna objekt i en modal men ändå skapa en deep link vid omladdning.

Lektion 4 av 413 steg

Bygga ett foto-modalt flöde med mjuk navigering är en gratis lektion i Next.js 15 fullstack (App Router + Server Actions) på CoddyKit. Detta är lektion 4 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Next.js 15 fullstack (App Router + Server Actions), och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Next.js 15 fullstack (App Router + Server Actions) innehåller totalt 4 lektioner.

Varför modaler förstör deep linking

Traditionella modalimplementationer renderar en dialog ovanpå den aktuella sidan. Det skapar ett grundläggande problem: när en användare delar URL:en eller uppdaterar webbläsaren försvinner modalen — ingen route ändras, så det finns inget bestående URL-state.

Next.js 15 löser detta med två avancerade routingfunktioner som fungerar tillsammans:

  • Intercepting Routes — fånga en navigering och rendera ett annat gränssnitt i den aktuella kontexten
  • Parallel Routes — rendera två route-segment samtidigt i samma layout

Tillsammans gör de det möjligt att öppna ett foto i en modal med en riktig URL (t.ex. /photos/42) samtidigt som rutnätet ligger synligt bakom. Vid en hård omladdning visar samma URL i stället fotot som en helsidesvy.

Filstrukturen du behöver

Mönstret kräver en specifik katalogstruktur i mappen app/. Här är strukturen för ett fotogalleri:

  • app/photos/page.tsx — fotonätet (listvyn)
  • app/photos/[id]/page.tsx — den fristående fotodetaljen i helsidesvy
  • app/@modal/(.)photos/[id]/page.tsx — den interceptade modalversionen
  • app/layout.tsx — rotlayouten som tar emot sloten modal

Prefixet @modal deklarerar en parallel route-slot. Prefixet (.) på det interceptande segmentet talar om för Next.js att fånga navigeringar till photos/[id] på samma djup i URL-trädet.

Den dubbla förekomsten — en riktig sida på photos/[id] och en interceptad version på @modal/(.)photos/[id] — är hela hemligheten.

Konfigurera rotlayouten för parallella slots

Rotlayouten (eller en annan delad layout) måste uppdateras så att den tar emot och renderar sloten modal. Next.js skickar varje parallel route-slot som en namngiven prop till layoutkomponenten.

Om ingen intercepting route är aktiv renderar Next.js en inbyggd default.tsx-fil för sloten. Utan den kraschar appen — tillhandahåll alltid en default.tsx som returnerar null.

// app/layout.tsx
import type { ReactNode } from 'react';

interface RootLayoutProps {
  children: ReactNode;
  modal: ReactNode; // named slot from @modal parallel route
}

export default function RootLayout({ children, modal }: RootLayoutProps) {
  return (
    <html lang="en">
      <body>
        {children}
        {modal} {/* renders null by default, or the intercepted modal */}
      </body>
    </html>
  );
}

// app/@modal/default.tsx
export default function ModalDefault() {
  return null; // no modal active — render nothing
}

Sidan med fotonätet

Fotonätet på app/photos/page.tsx hämtar alla foton och renderar dem som länkar. När en användare klickar på ett foto upptäcker Next.js att destinationen (/photos/[id]) har en intercepting counterpart och styr tyst renderingen till @modal/(.)photos/[id] i stället — utan att ändra URL:ens visade beteende.

Viktiga punkter:

  • Använd en vanlig <Link href="/photos/42"> — ingen särskild kod för modaltriggers behövs
  • Interceptingen styrs helt av filsystemet; rutnätskomponenten förblir ren
  • Om du markerar sidan med async kan du använda await för datahämtningen på komponentnivå
// app/photos/page.tsx
import Link from 'next/link';
import { getAllPhotos } from '@/lib/photos';

export default async function PhotosPage() {
  const photos = await getAllPhotos();

  return (
    <main className="grid grid-cols-3 gap-4 p-6">
      {photos.map((photo) => (
        <Link key={photo.id} href={`/photos/${photo.id}`}>
          <img
            src={photo.thumbnailUrl}
            alt={photo.title}
            className="w-full aspect-square object-cover rounded-lg hover:opacity-80 transition"
          />
        </Link>
      ))}
    </main>
  );
}

Fotodetaljen i helsidesvy (mål vid hård omladdning)

Routen på app/photos/[id]/page.tsx är den kanoniska destinationen. Den renderas när:

  • Användaren klistrar in URL:en direkt i adressfältet
  • Användaren uppdaterar sidan medan modalen är öppen
  • En sökmotors crawler indexerar fotot

Den här sidan är en vanlig asynkron Next.js Server Component – den tar emot prop:en params och hämtar sina egna data. Här behövs ingen särskild kunskap om intercepting routes.

// app/photos/[id]/page.tsx
import { getPhotoById } from '@/lib/photos';
import { notFound } from 'next/navigation';

interface PhotoPageProps {
  params: Promise<{ id: string }>;
}

export default async function PhotoPage({ params }: PhotoPageProps) {
  const { id } = await params; // params is a Promise in Next.js 15
  const photo = await getPhotoById(id);

  if (!photo) notFound();

  return (
    <main className="max-w-3xl mx-auto p-8">
      <img src={photo.imageUrl} alt={photo.title} className="w-full rounded-xl" />
      <h1 className="mt-4 text-2xl font-bold">{photo.title}</h1>
      <p className="mt-2 text-gray-600">{photo.description}</p>
    </main>
  );
}

Den interceptande modalrouten

Filen på app/@modal/(.)photos/[id]/page.tsx renderas i stället för helsidesvyn när navigeringen sker på klientsidan från bildrutnätet. Segmentet (.) betyder intercepta en nivå upp – det matchar photos/[id] i förhållande till djupet i det aktuella routeträdet.

Notation för interceptdjup:

  • (.) – samma nivå
  • (..) – en nivå upp
  • (...) – från roten

Den här komponenten renderar ett dialogöverlägg och återanvänder samma dataladdningslogik som helsidesvyn, så att innehållet förblir konsekvent oavsett hur användaren kom dit.

// app/@modal/(.)photos/[id]/page.tsx
import { getPhotoById } from '@/lib/photos';
import { notFound } from 'next/navigation';
import PhotoModal from '@/components/PhotoModal';

interface InterceptedPhotoPageProps {
  params: Promise<{ id: string }>;
}

export default async function InterceptedPhotoPage({
  params,
}: InterceptedPhotoPageProps) {
  const { id } = await params;
  const photo = await getPhotoById(id);

  if (!photo) notFound();

  return <PhotoModal photo={photo} />;
}

Bygga modalens Client Component

Komponenten PhotoModal måste stänga modalen när användaren klickar på bakgrunden eller på en stängningsknapp. I en konfiguration med parallella och interceptande routes innebär stängning att navigera tillbaka – då återställs det underliggande bildrutnätet och modalens slot töms.

Använd useRouter().back() från next/navigation för detta. Märk komponenten med 'use client' eftersom den använder webbläsarens hooks. Dialogelementet använder det inbyggda HTML-API:et <dialog> för tillgänglig modal semantik.

'use client';

import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';
import type { Photo } from '@/lib/photos';

interface PhotoModalProps {
  photo: Photo;
}

export default function PhotoModal({ photo }: PhotoModalProps) {
  const router = useRouter();
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    dialogRef.current?.showModal();
  }, []);

  function handleClose() {
    router.back(); // removes modal from parallel slot
  }

  return (
    <dialog
      ref={dialogRef}
      onClose={handleClose}
      className="backdrop:bg-black/60 rounded-2xl p-0 max-w-2xl w-full"
    >
      <button
        onClick={handleClose}
        className="absolute top-3 right-3 text-white text-xl"
        aria-label="Close"
      >
        ✕
      </button>
      <img src={photo.imageUrl} alt={photo.title} className="w-full rounded-t-2xl" />
      <div className="p-6">
        <h2 className="text-xl font-bold">{photo.title}</h2>
        <p className="mt-2 text-gray-600">{photo.description}</p>
      </div>
    </dialog>
  );
}

Lägga till ett laddningstillstånd för modalens slot

Eftersom den interceptande routen hämtar data på serversidan innan den strömmas till klienten kan det uppstå en synlig fördröjning. Next.js har stöd för samplacerade loading.tsx-filer inuti parallella route-slots, så att en skeleton kan visas medan serverkomponenten körs klart.

Placera filen på app/@modal/(.)photos/[id]/loading.tsx. Detta följer samma Suspense-baserade strömning som gäller för alla routesegment – parallella slots är inget undantag.

// app/@modal/(.)photos/[id]/loading.tsx
export default function ModalLoading() {
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
      <div className="bg-white rounded-2xl p-8 max-w-2xl w-full animate-pulse">
        <div className="h-72 bg-gray-200 rounded-xl mb-4" />
        <div className="h-6 bg-gray-200 rounded w-2/3 mb-2" />
        <div className="h-4 bg-gray-100 rounded w-full" />
      </div>
    </div>
  );
}

Hantera Error Boundaries i modalens slot

Om hämtningen av fotot misslyckas (till exempel på grund av ett ogiltigt ID eller ett nätverksfel) bör den interceptade routen hantera felet på ett smidigt sätt. Lägg till en fil med namnet error.tsx i det interceptande segmentet – den måste vara en Client Component eftersom den tar emot objektet error och funktionen reset.

Felgränsen är begränsad till den parallella sloten, så ett fel i modalen kraschar inte bildrutnätet bakom den.

'use client';

// app/@modal/(.)photos/[id]/error.tsx
import { useRouter } from 'next/navigation';

interface ModalErrorProps {
  error: Error & { digest?: string };
  reset: () => void;
}

export default function ModalError({ error, reset }: ModalErrorProps) {
  const router = useRouter();

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
      <div className="bg-white rounded-2xl p-8 text-center">
        <h2 className="text-lg font-semibold text-red-600">Failed to load photo</h2>
        <p className="mt-2 text-sm text-gray-500">{error.message}</p>
        <div className="mt-4 flex gap-3 justify-center">
          <button onClick={reset} className="px-4 py-2 bg-blue-500 text-white rounded-lg">
            Try again
          </button>
          <button onClick={() => router.back()} className="px-4 py-2 bg-gray-200 rounded-lg">
            Go back
          </button>
        </div>
      </div>
    </div>
  );
}

Generera metadata för delning i sociala medier

En stor fördel med det här mönstret är att helsidesrouten på app/photos/[id]/page.tsx kan exportera omfattande Open Graph-metadata. När en användare delar URL:en går crawlers till den kanoniska routen – inte till modalen – och får korrekta <meta>-taggar.

Exportera en funktion med namnet generateMetadata från helsidesrouten. Den interceptade modal sidan behöver inte exportera egen metadata eftersom crawlers aldrig når den direkt.

// app/photos/[id]/page.tsx  (metadata addition)
import type { Metadata } from 'next';
import { getPhotoById } from '@/lib/photos';

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const photo = await getPhotoById(id);

  if (!photo) return { title: 'Photo not found' };

  return {
    title: photo.title,
    description: photo.description,
    openGraph: {
      title: photo.title,
      description: photo.description,
      images: [{ url: photo.imageUrl, width: 1200, height: 630 }],
    },
  };
}

Testa beteendet vid mjuk navigering och hård uppdatering

När strukturen är på plats ska Ni manuellt verifiera de två separata kodvägarna:

  • Mjuk navigering (förväntat: modal): Börja på /photos och klicka på valfri bild. URL:en ändras till /photos/42, men rutnätet förblir synligt under modalens överlägg. Sloten @modal är aktiv.
  • Hård uppdatering (förväntat: helsida): Tryck på F5 eller Cmd+R medan modalen är öppen. Sidan laddas om och app/photos/[id]/page.tsx renderas i sin helhet – inget rutnät och ingen modalomslutning – som om Ni hade navigerat direkt dit.
  • Tillbakasnavigering (förväntat: rutnät): Klicka på stängningsknappen eller webbläsarens bakåtknapp. router.back() tar bort historikposten, modalens slot töms och rutnätet visas igen utan en fullständig omladdning.

Detta beteende med tre möjliga vägar är det utmärkande kännetecknet för den kombinerade lösningen och bör vara ert främsta acceptanskriterium under utvecklingen.

Snabbkontroll: notation för interceptande routedjup

Ni bygger en dashboard på app/dashboard/ som innehåller en teamlista på app/dashboard/team/[memberId]/page.tsx. Ni vill intercepta navigeringar till en medlems profil och visa den i en modal samtidigt som dashboarden förblir synlig. Var ska Ni placera filen för den interceptande routen?

Sammanfattning: det kompletta mönstret för fotomodal

I den här lektionen byggde Ni ett komplett fotogalleri med modaler vid mjuk navigering och URL:er som kan öppnas direkt, med hjälp av två kompletterande funktioner i Next.js 15:

  • Parallel Routes (@modal) – skapa en namngiven slot i layouten som kan renderas självständigt bredvid children och som får standardvärdet null via default.tsx
  • Intercepting Routes ((.), (..), (...)) – fånga upp en navigering på klientsidan och ersätta den med en annan komponent utan att ändra URL:en

De viktigaste insikterna att komma ihåg:

  • Den kanoniska routen (photos/[id]/page.tsx) hanterar alltid hårda uppdateringar och sökmotorscrawlers – komplettera den med metadata
  • Den interceptande routen (@modal/(.)photos/[id]/page.tsx) aktiveras endast vid mjuk navigering inifrån appen
  • När modalen stängs anropas router.back() – ingen egen tillståndshantering krävs
  • Lägg till loading.tsx och error.tsx i det interceptande segmentet för att hantera asynkrona tillstånd på ett smidigt sätt

Det här mönstret ger användarupplevelsen hos en single-page-app samtidigt som det bevarar fördelarna med delbarhet och SEO från en flersidesarkitektur.

Gratis att börja

Lär dig TypeScript med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
22
Lektioner
88

Vanliga frågor

Är lektionen ”Bygga ett foto-modalt flöde med mjuk navigering” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Next.js 15 fullstack (App Router + Server Actions), inklusive ”Bygga ett foto-modalt flöde med mjuk navigering”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Next.js 15 fullstack (App Router + Server Actions) innehåller totalt 4 lektioner.

Vad lär jag mig i ”Bygga ett foto-modalt flöde med mjuk navigering”?

Kombinera interception och parallella routes för att öppna objekt i en modal men ändå skapa en deep link vid omladdning. Ni övar på Next.js 15 fullstack (App Router + Server Actions) med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Next.js 15 fullstack (App Router + Server Actions)?

Du behöver inga förkunskaper. Utbildningen i Next.js 15 fullstack (App Router + Server Actions) på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 4 av 4.

Hur lång tid tar lektionen ”Bygga ett foto-modalt flöde med mjuk navigering”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Next.js 15 fullstack (App Router + Server Actions)-lektionen?

Ja. Varje Next.js 15 fullstack (App Router + Server Actions)-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Parallella routes med namngivna slots och standardsegment
  2. Intercepting routes för delade modalupplevelser
  3. Villkorad slot-rendering för instrumentpaneler och flikar
  4. Bygga ett foto-modalt flöde med mjuk navigering
← Tillbaka till Next.js 15 fullstack (App Router + Server Actions)