// app/layout.tsx
export default function Layout({
children,
modal
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html>
<body>
{children}
{modal}
<div id="modal-root" />
</body>
</html>
);
}
// app/@modal/(.)photos/[id]/page.tsx
import Modal from '@/components/Modal';
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<img src={`/api/photos/${params.id}`} alt="Full size" />
</Modal>
);
}M-am chinuit vreo două zile să înțeleg flow-ul ăsta de Parallel și Intercepting Routes până când a făcut click. Dacă vrei un modal care se deschide instant când dai click pe o imagine, dar care are și URL propriu când dai refresh sau share la link, structura asta e singura cale curată în Next.js App Router.
Recent, la un proiect de portofoliu cu vreo 12.000 de asset-uri media, clientul voia exact experiența de pe Instagram. Adică dai click pe poză, apare modalul peste listă, URL-ul se schimbă în /photos/123, dar lista din spate rămâne acolo, nemișcată. Dacă dai refresh la /photos/123, vrei să vezi pagina de detaliu full-screen, nu doar modalul peste nimic.
Magia sloturilor și a parantezelor
Secretul stă în folderul @modal. Ăsta e un „slot”. Next.js îl injectează automat ca prop în layout-ul tău principal. Dar marea șmecherie apare când combini asta cu Intercepting Routes, folosind convenția (.)photos. Punctul ăla din paranteză îi zice lui Next: „Dacă userul navighează aici din interiorul aplicației (client-side), interceptează ruta și randează folderul ăsta în loc de pagina reală”.
Am pățit de multe ori să uit de fișierul default.tsx. E critic. Dacă nu îl ai în folderul @modal, Next.js o să dea 404 sau o să crape la refresh, pentru că nu știe ce să randeze în slotul de modal atunci când ruta nu este interceptată. Practic, default.tsx trebuie să returneze null ca să nu afișeze nimic când nu ești pe o rută de tip modal.
De ce e mai bun decât un simplu state de React?
Am economisit cam 30% din timpul de dezvoltare pe partea de UX logic pentru că nu mai trebuie să gestionez manual starea isOpen sau să mă chinui cu useEffect ca să schimb URL-ul prin window.history.pushState. Next.js se ocupă de tot stack-ul de navigare.
Totuși, există un trade-off sincer: debugging-ul devine un coșmar dacă ai layout-uri imbricate complex. Uneori, cache-ul de client-side o ia razna și te trezești că modalul rămâne agățat deși ai schimbat pagina. De asemenea, trebuie să fii foarte atent la cum închizi modalul. Un simplu router.back() e de obicei suficient, dar dacă userul a ajuns pe modal direct via link extern, router.back() s-ar putea să-l trimită pe Google sau pe pagina de unde a venit în loc să închidă modalul și să-i arate galeria.
Implementarea în Layout
În layout.tsx, trebuie să accepți prop-ul @modal (sau cum l-ai numit) pe lângă children. E important să le randezi pe amândouă în același loc, de obicei modalul fiind poziționat absolut sau fix peste restul conținutului.
La proiectul menționat, am observat că performanța a crescut pentru că nu mai re-randam tot arborele de componente al paginii de galerie. Modalul e doar un layer subțire peste ceva ce e deja în memorie. Utilizatorul simte că aplicația e „nativă”, zero flickering, zero loading spinners inutile pe date pe care deja le avem în pagină.
Voi cum gestionați închiderea modalului când userul vine de pe un link extern? Mergeți pe un fallback manual la o rută părinte sau vă bazați pe istoricul browserului?