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

Modaluri ca pe Instagram în Next.js: Parallel și Intercepting Routes explicate

De Ana Ionescu, 11 oct. 2026 · 5 vizualizări · 3 like-uri

Postat acum 5 zile
typescript
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal';
import PhotoView from '@/components/PhotoView';

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

export default async function InterceptedPhotoPage({ params }: Props) {
  const { id } = await params;
  
  return (
    <Modal>
      <PhotoView photoId={id} />
    </Modal>
  );
}

Dacă ai încercat vreodată să faci UX tip feed de Instagram în Next.js — click pe o poză din listă, se deschide un modal peste feed cu URL schimbat în /photos/123, dar la refresh randează pagina completă fără layout-ul feed-ului —, știi ce coșmar era în Pages Router. Combinația de Parallel Routes (@slot) și Intercepting Routes ((.)photos) este răspunsul oficial din App Router. Am migrat pattern-ul ăsta la începutul anului pe o platformă cu vreo 14k utilizatori activi zilnic și am scăpat definitiv de hack-urile cu shallow routing și history.pushState.

Cum funcționează de fapt convenția de foldere

Toată magia (și 90% din confuzie) stă în structura de fișiere. Trebuie să creezi două rute pentru aceeași resursă: una standalone și una interceptată.

Ai nevoie de:

  1. app/photos/[id]/page.tsx — Pagina normală, completă. Asta se încarcă dacă cineva dă refresh direct pe URL sau trimite linkul pe Slack.
  2. app/@modal/(.)photos/[id]/page.tsx — Ruta interceptată. Convenția (.) îi spune lui Next.js: „când utilizatorul navighează din pagina părinte (același nivel) către /photos/[id], nu schimba layout-ul principal, ci injectează conținutul în slotul @modal”.
  3. app/layout.tsx — Unde primești @modal ca prop pe lângă children.

Partea critică pe care o ratează aproape toată lumea la primul setup este fișierul default.tsx. Dacă nu pui un app/@modal/default.tsx care returnează null, o să ai erori 404 sau layout-ul o să se comporte ciudat la navigări ulterioare, pentru că Next.js nu știe ce să randeze în slotul paralel când utilizatorul e pe pagina principală.

Capcana de care m-am lovit: Server Components și datele din background

Când interceptezi ruta, Next.js păstrează pagina curentă în background (children) și montează componenta din @modal. Asta e superb pentru performanță: feed-ul din spate nu se re-randează și nu pierde poziția de scroll.

Problema apare când modalul face mutații de date. Am avut un caz clasic: userul dădea „Like” la poză în modal. Dacă folosești revalidatePath('/photos/[id]') într-un Server Action, Next.js va revalida resursa interceptată, dar feed-ul din background nu știe automat că numărul de like-uri s-a schimbat până nu revalidezi explicit și ruta părinte. Asta înseamnă că la închiderea modalului, feed-ul arăta starea veche dacă nu sincronizam starea pe client sau nu făceam revalidare la ambele rute.

Trade-off-uri fără menajamente

Pattern-ul este impecabil din perspectiva utilizatorului final: router.back() închide modalul firesc, URL-ul e share-able, iar performanța la navigare e instantă.

Dar din perspectiva mentenanței de cod, e mai greu de urmărit. Ai duplicare de layout logic între pagina de sine stătătoare și modal. Dacă vrei ca ambele să arate identic ca și conținut (dar împachetate diferit), trebuie să extragi toată logica vizuală într-o componentă comună (<PhotoView />), altfel ajungi să modifici în două locuri la fiecare bug fix.

De asemenea, dacă ai rute adânci, convențiile de paranteze ((.), (..), (...)) devin rapid ilizibile dacă nu ești extrem de atent la ierarhia de foldere.

Voi cum gestionați închiderea modalului? Mergeți pe router.back() clasic sau preferați un fallback explicit la ruta părinte când utilizatorul vine direct din extern?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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