// app/feed/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal';
import { PhotoDetail } from '@/components/PhotoDetail';
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function PhotoModal({ params }: PageProps) {
const { id } = await params;
const photo = await getPhotoById(id);
if (!photo) {
return null;
}
return (
<Modal>
<PhotoDetail photo={photo} isModal={true} />
</Modal>
);
}Am avut de rezolvat recent o problemă clasică de UX la un proiect cu peste 15k de imagini de produs. Userul dă click pe o poză din feed, vrea să vadă detaliile într-un modal fără să piardă poziția de scroll, dar dacă trimite URL-ul /photos/123 altcuiva, acel link trebuie să deschidă pagina completă. În vechiul pages/ făceam tot felul de hacks cu router.push(..., { shallow: true }) și stare locală. În App Router, soluția curată se bazează pe Parallel Routes combinate cu Intercepting Routes.
Cum se leagă magia în folder structure
Ideea e simplă, dar structura de fișiere îi sperie pe mulți prima dată când o văd. Ai nevoie de o rută paralelă (notată cu @modal) în layout-ul părinte și de o rută interceptată (notată cu (.)photos).
Structura arată cam așa:
app/feed/layout.tsx-> primește propschildrenșimodal.app/feed/@modal/(.)photos/[id]/page.tsx-> codul care randează modalul peste feed.app/photos/[id]/page.tsx-> pagina standalone pentru când dai hard refresh sau deschizi linkul direct într-un tab nou.
Când userul navighează din feed prin <Link href="/photos/123">, Next.js interceptează tranziția pe client și aruncă componenta din (.)photos în slotul @modal. Dacă dă F5, Next.js nu mai are contextul de navigație din browser și randează direct pagina completă din app/photos/[id].
Unde te lovești de perete: Trade-off-uri reale
Evident, nu totul e lapte și miere. Am economisit cam 300ms la tranziție pentru că nu reîncărcăm tot layout-ul de fundal, dar dai de alte bătăi de cap în producție.
În primul rând, gestionarea butonului de închidere. Dacă userul închide modalul cu un buton de X, folosești de obicei router.back(). Dar ce faci dacă omul a ajuns pe acel modal direct dintr-un link extern sau din refresh? Un router.back() îl va trimite direct pe Google sau pe site-ul de unde a venit. Trebuie să verifici istoricul sau să faci fallback la router.push('/feed').
În al doilea rând, fișierele default.tsx. Când rutele paralele nu au ce să afișeze (de exemplu când ești doar pe /feed fără niciun modal deschis), trebuie să ai neapărat un default.tsx care să returneze null în folderul @modal. Altfel, la primul refresh sau navigare adiacentă primești un unhandled 404 pe slotul respectiv.
Ce alegem și când
Metoda asta e excelentă pentru feed-uri de imagini, previzualizări de produse sau dashboard-uri complexe unde contextul paginii din spate e critic. Totuși, dacă ai un modal simplu de "Confirmare ștergere" sau "Editare profil", nu te complica cu rute interceptate. Starea locală cu React state sau useSearchParams e arhi-suficientă și îți salvează ore bune de debugging.
Voi ce abordare folosiți pentru modaluri cu URL în App Router? Ați rămas pe useSearchParams sau ați trecut complet pe intercepting routes?