eduardweb.
App RouterAvansat#architecture#nextjs#react#frontend#app-router

Cum faci un Modal cu URL propriu în Next.js fără Page Reload (Parallel & Intercepting Routes)

De Bogdan Răducanu, 24 iul. 2026 · 12 vizualizări · 2 like-uri

Postat 24 iul. 2026
typescript
import { ModalWrapper } from '@/components/modal-wrapper';
import { getPhoto } from '@/lib/api';
import Image from 'next/image';

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

export default async function PhotoModal({ params }: Props) {
  const { id } = await params;
  const photo = await getPhoto(id);

  return (
    <ModalWrapper>
      <div className="relative h-[80vh] w-[80vw] max-w-4xl bg-black rounded-lg overflow-hidden">
        <Image
          src={photo.url}
          alt={photo.title}
          fill
          className="object-contain"
          priority
        />
        <div className="absolute bottom-0 p-4 bg-gradient-to-t from-black/80 to-transparent w-full text-white">
          <h2 className="text-lg font-bold">{photo.title}</h2>
        </div>
      </div>
    </ModalWrapper>
  );
}

Am implementat recent pattern-ul ăsta la o platformă de imobiliare cu peste 15k de imagini indexate, unde UX-ul trebuia să fie impecabil. Voiam ca userul să dea click pe o poză din galerie, să se deschidă un modal instant, URL-ul să se schimbe în /photos/123, dar dacă dă Refresh (F5) sau trimite linkul pe WhatsApp, să se încarce pagina dedicată completă, nu modalul.

În vechiul pages/ router rezolvam asta cu tot felul de hacks urâte în query și state global care crăpau la SSR sau încurcau SEO-ul. Cu App Router din Next.js, combinația dintre Parallel Routes (sloturi cu @) și Intercepting Routes (foldere cu sintaxa (.) sau (..)) e soluția nativă, deși prima dată când te uiți pe structura de directoare zici că e un puzzle scăpat pe jos.

Structura de foldere care te zăpăcește la început

Ca să meargă treaba fără bâlbâieli, ai nevoie de un slot paralel în layout.tsx-ul părinte și de folderul de interceptare definit exact la nivelul de adâncime potrivit.

Dacă vrei ca din pagina principală sau din /feed să interceptezi ruta /photos/[id], structura în app/ arată cam așa:

app/
├── @modal/
│   └── (.)photos/
│       └── [id]/
│           └── page.tsx
├── photos/
│   └── [id]/
│       └── page.tsx
├── default.tsx
├── layout.tsx
└── page.tsx

Sintaxa (.) îi spune lui Next.js: „când navighezi din interiorul aplicației (client-side transition) către o rută din același nivel numită photos/[id], nu încărca pagina întreagă, ci randează componenta asta direct în slotul @modal”.

Legătura din Layout și capcana cu default.tsx

În app/layout.tsx, trebuie să primești slotul @modal ca prop alături de children. Modificarea e banală: randezi {children} și dedesubt {modal}.

Aici vine însă greșeala pe care am văzut-o la vreo trei colegi juniori în ultimele luni: uită să definească fișierul default.tsx.

Când nu ai niciun modal activ (adică ești pe homepage fără niciun pop-up deschis), Next.js trebuie să știe ce să pună în slotul @modal. Dacă nu găsește un default.tsx care să returneze null (atât în rădăcină, cât și în @modal), aplicația va arunca eroare 404 la prima navigare directă sau la un simplu hard-refresh.

Trade-off-uri reale și performanță

Pe partea de viteză, rezultatele sunt fantastice. La proiectul nostru am scăzut latența percepută la deschiderea imaginii de la ~350ms (când routerul trebuia să schimbe tot layout-ul) la sub 40ms, pentru că facem re-render doar pe componenta de modal peste lista deja aflată în memorie.

Dar sunt câteva minusuri pe care trebuie să le știi înainte să le promiți clienților că durează o oră implementarea:

  1. Managementul stării de închidere: Ca să închizi modalul, trebuie să apelezi router.back(). Dacă userul a ajuns pe acel modal prin copy-paste la URL (link direct), router.back() îl va arunca înapoi pe Google sau pe alt site extern. A trebuit să scriem o logică custom care verifică stiva din window.history și face router.push('/') ca fallback.
  2. Framer Motion și Server Components: Pagina interceptată poate fi un Server Component unde faci fetch direct la DB, dar dacă vrei animații la Exit (un-mount), va trebui să împachetezi totul într-un Client Component wrapper cu 'use client'.
  3. Router Cache: Cache-ul agresiv din App Router poate reține starea modalului în mod neașteptat când navighezi rapid cu săgețile Stânga/Dreapta între poze.

Voi cum gestionați cazurile astea de dialoguri cu URL dedicat? Ați trecut pe intercepting routes în producție sau folosiți în continuare useSearchParams?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.