// app/layout.tsx
import React from 'react';
interface LayoutProps {
children: React.ReactNode;
modal: React.ReactNode;
}
export default function RootLayout({ children, modal }: LayoutProps) {
return (
<html lang="ro">
<body className="bg-slate-900 text-white min-h-screen">
{/* Pagina principală */}
<main className="container mx-auto p-4">{children}</main>
{/* Slotul paralel unde Next.js va injecta modalul interceptat */}
{modal}
</body>
</html>
);
}Dacă ai încercat vreodată să faci un modal stil Instagram sau Pinterest în Next.js App Router — unde URL-ul se schimbă în /photos/123, dar pagina din spate rămâne intactă — știi deja că soluțiile clasice cu useState sunt o cârpeală. Am implementat pattern-ul ăsta la o platformă de e-commerce cu vreo 12k vizite zilnice pe galeria de produse și am scăpat de 200ms de re-render inutil pe layout-ul părinte. Câștigi navigare instantanee, SEO ok și URL-uri pe care le poți da mai departe fără să rupi UX-ul.
Structura de dosare care îți prinde urechile
Chestia asta e super confuză la început din cauza convențiilor de denumire din App Router. Ca să meargă din prima, ai nevoie de două concepte combinate: Slot-uri Paralele (Parallel Routes) și Rute Interceptate (Intercepting Routes).
Să zicem că ai pagina principală în app/page.tsx și pagina dedicată a pozei în app/photos/[id]/page.tsx. Ca să pui modalul peste pagina curentă când utilizatorul dă click pe o poză, faci un slot @modal în interiorul aceluiași layout.
Calea devine: app/@modal/(.)photos/[id]/page.tsx. Paranteza (.) îi spune lui Next.js: "când navighezi din client (cu <Link>) către /photos/[id], nu încărca pagina întreagă, ci interceptează ruta și arunc-o în slotul @modal".
Ce faci cu default.tsx și de ce crapă aplicația fără el
Aici m-am lovit eu prima dată. Dacă nu pui un fișier default.tsx în app/@modal/, Next.js o să-ți arunce un 404 sau un crash urât la hărți când dai hard refresh sau când navighezi pe o rută unde slotul @modal nu are potrivire.
default.tsx este fallback-ul pentru sloturile paralele atunci când routerul nu găsește nicio potrivire activă pe slotul respectiv. Când nu e niciun modal deschis, default.tsx trebuie să returneze pur și simplu null:
export default function DefaultModal() {
return null;
}
Fără fișierul ăsta, starea slotului devine undefined la soft navigation pe alte rute de nivel înalt.
Trade-off-uri reale și bube din producție
Să fim sinceri, soluția asta e genială pentru UX, dar vine cu niște costuri de mentenanță pe care trebuie să le cântărești bine înainte să o bagi în codul de producție:
- Iadul directoarelor: Când ai rute adânci gen
@modal/(..)dashboard/settings/(.)photos/[id], structura de dosare începe să arate ca un regex scris la supărare. Colegii mai juniori din echipă se vor pierde sigur în ea. - Gestionarea butonului de Close: Ca să închizi modalul, de obicei apelezi
router.back(). Dar dacă userul a ajuns pe/photos/123direct prin copy-paste la URL (hard navigation), nu există istoric anterior! În cazul ăsta,router.back()îl aruncă pe Google sau nu face nimic. Am fost nevoiți să scriem o verificare explicită: dacă istoricul e gol, facemrouter.push('/')manual. - Scroll restoration: Pe mobil (mai ales iOS Safari), am văzut cazuri unde scroll-ul paginii din spate sare în sus când deschizi sau închizi modalul, mai ales dacă ai liste virtualizate.
E cea mai curată abordare din punct de vedere arhitectural dacă vrei un modal ruteabil și indexabil separat, dar nu o folosi pentru dialoguri simple de confirmare sau formulare mici. Voi ați trecut pe rute interceptate în producție sau încă mergeți pe clasicul query param gen ?modal=photo-123?