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

Modal-uri cu URL dedicat în Next.js App Router: Parallel și Intercepting Routes fără dureri de cap

De Cosmin Rotaru, 2 aug. 2026 · 8 vizualizări · 3 like-uri

Postat 2 aug. 2026
typescript
// app/@modal/(.)photos/[id]/page.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';

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

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

  const onDismiss = () => {
    router.back();
  };

  return (
    <dialog
      ref={dialogRef}
      onClose={onDismiss}
      className="backdrop:bg-black/80 rounded-lg p-6 bg-slate-900 text-white border border-slate-800"
    >
      <div className="flex flex-col gap-4">
        <img src={`/api/photos/${params.id}`} alt="Preview" className="max-w-xl rounded" />
        <button
          onClick={onDismiss}
          className="px-4 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded font-medium self-end"
        >
          Închide
        </button>
      </div>
    </dialog>
  );
}

Am avut de făcut recent pe un proiect cu peste 15k imagini un feed de galerii de tip Pinterest. Cerința era simplă la prima vedere: când userul dă click pe o poză din feed, deschizi un modal cu detalii fără să pierzi scroll-ul, dar schimbi URL-ul în /photos/123. Dacă cineva ia acel URL și îl trimite pe Slack, primitorul trebuie să vadă o pagină de sine stătătoare, nu un modal plutind peste un fundal gol.

Soluția veche versus pattern-ul din App Router

În trecut rezolvam mizeria asta cu un query param gen ?photoId=123 și un global state gestionat de Zustand sau Redux. Era un hack de doi lei. Nu aveai SSR curat pe pagina de detalii, iar SEO-ul suferea groaznic pentru că paginile individuale nu erau randate ca entități separate pentru crawlere.

În App Router, combinația dintre @modal (Parallel Routes) și (.)photos/[id] (Intercepting Routes) rezolvă problema asta elegant, direct la nivel de router.

Arhitectura folderelor – unde se încurcă lumea

Marea schemă e că trebuie să respecți cu sfințenie ierarhia. Dacă feed-ul tău e în app/page.tsx, layout-ul din app/layout.tsx trebuie să primească @modal ca prop alături de children.

Structura curată pe care am folosit-o eu arată așa:

  • app/layout.tsx (randare children + modal slot)
  • app/default.tsx (returnează null când modalul nu e activ)
  • app/@modal/default.tsx (returnează null pentru fallback-ul slotului)
  • app/@modal/(.)photos/[id]/page.tsx (interceptează navigarea client-side)
  • app/photos/[id]/page.tsx (pagina completă, randată la F5 sau acces direct)

Partea unde am pierdut eu două ore la primul proiect: convenția de paranteze. (.) înseamnă "interceptează la același nivel", (..) înseamnă "un nivel mai sus". Dacă pui folderul greșit, Next.js ignoră interceptarea și face hard navigation, reîncărcând tot layout-ul.

Cum închizi modalul curat fără bug-uri de URL

Problema clasică la intercepting routes apare când utilizatorul apasă pe overlay sau pe butonul X. Dacă faci doar un setShowModal(false) intern, URL-ul rămâne blocat pe /photos/123 deși modalul a dispărut.

Varianta corectă este să apelezi router.back(). Asta face pop în istoricul browser-ului, URL-ul revine la cel anterior, iar Next.js golește automat slotul @modal (randând default.tsx-ul care dă null).

Trade-off-uri și ce am învățat pe pielea mea

UX-ul este absolut superb. Pe testele noastre am economisit în medie 300ms la afișarea detaliilor pentru că nu mai remonta layout-ul root și nu mai re-evalua querie-urile din feed. Scroll-ul rămâne exact unde l-ai lăsat.

Ce e nasol? Gestionarea de state e mai complicată dacă ai formulare în modal. Dacă folosești Server Actions în interiorul modalului interceptat și dai revalidatePath, uneori Next.js va încerca să re-randeze și pagina din spate, ceea ce poate duce la flickering dacă nu ești atent la caching.

De asemenea, debugging-ul pe server logs devine puțin ambiguu, pentru că un request de client-side navigation va executa doar componenta din slotul interceptat, nu toată pagina.

Ați trecut deja la pattern-ul ăsta în producție sau preferați în continuare abordarea clasică cu searchParams ?modal=true?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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