// app/feed/@modal/(.)photos/[id]/page.tsx
'use client';
import { useRouter } from 'next/navigation';
import { Dialog } from '@/components/ui/dialog';
export default function PhotoModal({ params }: { params: { id: string } }) {
const router = useRouter();
const handleClose = () => {
// Daca avem istoric in tab, dam back. Altfel redirectam la feed.
if (window.history.length > 2) {
router.back();
} else {
router.push('/feed');
}
};
return (
<Dialog open={true} onOpenChange={handleClose}>
<div className="p-6 bg-slate-900 text-white rounded-xl">
<h2 className="text-xl font-bold">Photo #{params.id}</h2>
<img src={`/api/photos/${params.id}`} alt="Preview" className="max-h-[70vh] object-contain my-4" />
<button onClick={handleClose} className="px-4 py-2 bg-red-600 rounded-lg">
Închide
</button>
</div>
</Dialog>
);
}Am implementat recent o galerie foto pe un proiect de imobiliare cu peste 12.000 de randări 3D. Clienții voiau ca la click pe o poză să apară un modal rapid, dar dacă trimit URL-ul /photos/123 pe WhatsApp, destinatarul să vadă imaginea pe o pagină completă, dedicată.
Rezolvarea elegantă în App Router combină Parallel Routes (folosind sloturi @modal) cu Intercepting Routes (folosind convenția de foldere (.)). Pare magie la început, dar devine rapid un coșmar dacă nu înțelegi cum gestionează Next.js starea aplicației pe client.
Cum organizezi folderele fără să-ți prinzi urechile
Ca să interceptezi ruta /photos/[id], trebuie să plasezi slotul paralel în același layout părinte unde se află și galeria foto.
Structura arată așa:
app/feed/layout.tsx(primește props{ children, modal })app/feed/page.tsx(lista de poze)app/feed/@modal/(.)photos/[id]/page.tsx(modalul interceptat)app/feed/default.tsxșiapp/feed/@modal/default.tsxapp/photos/[id]/page.tsx(pagina de fallback pentru hard navigation)
Convenția (.) îi spune routerului să intercepteze segmentul aflat pe același nivel de rutare. Când utilizatorul apasă pe un <Link href="/photos/42"> din interiorul /feed, Next.js nu mai schimbă toată pagina. În schimb, injectează componenta din (.)photos/[id] direct în prop-ul modal din layout.
Trapa clasică: De ce primești 404 sau layout gol la refresh
Cea mai frecventă greșeală pe care o văd la devii care trec pe App Router e lipsa fișierului default.tsx.
Parallel routes au nevoie de un fallback pentru starea inițială sau când faci hard navigation (F5 / copy-paste URL). Dacă dai refresh pe /feed, slotul @modal nu are ce să afișeze. Fără un default.tsx care să returneze null, Next.js va arunca o eroare 404 pe tot layout-ul.
Am rezolvat simplu: am creat app/feed/@modal/default.tsx care returnează pur și simplu null. Astfel, când cineva intră direct pe /feed, slotul modalului e gol. Când navighează pe o poză, slotul se populează cu modalul.
Închiderea modalului și UX-ul pe mobil
Ca să închizi modalul, opțiunea evidentă este router.back(). Asta restabilește URL-ul /feed și golește slotul @modal.
Totuși, am observat un caz ciudat în producție: dacă un user ajunge pe modal direct printr-un link extern și apoi apasă pe butonul de închidere (care apelează router.back()), riscă să părăsească site-ul complet dacă nu există istoric anterior în tab-ul respectiv.
Trade-off-ul e evident. Pentru o experiență curată, verificăm dacă există istoric în browser. Dacă utilizatorul a venit direct pe acel link, facem router.push('/feed') în loc de router.back().
Ce am câștigat după refactoring
Am scăzut timpul de percepție a încărcării la sub 30ms pe navigare internă, deoarece browserul nu mai distruge și nu mai re-evaluează componentele grele din galeria foto. În plus, starea de scroll se păstrează intactă sub modal.
Voi cum gestionați starea asta? Ați rămas pe soluții clasice cu query params gen ?modal=photo-123 sau ați trecut complet pe intercepting routes?