// app/layout.tsx
import React from 'react';
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html lang="ro">
<body>
<main>{children}</main>
{/* Slot-ul paralel pentru modal */}
<div id="modal-root">{modal}</div>
</body>
</html>
);
}
// app/@modal/default.tsx
export default function Default() {
// Obligatoriu! Returnează null când niciun modal nu e activ.
return null;
}M-am lovit de cerința asta la un proiect e-commerce cu vreo 14k de produse: clientul voia ca din lista principală, când dai click pe o poză, să se deschidă un modal rapid cu URL schimbat în /photos/[id], fără să piardă scroll-ul sau contextul din spate. În același timp, dacă cineva dă copy-paste la acel URL într-un tab nou sau dă Refresh (F5), aplicația trebuie să randeze pagina completă de produs, nu un modal urât pe fundal alb.
În Pages Router era o varză totală de router.push cu as prop și stare globală. În App Router, soluția nativă este combinația dintre Parallel Routes (slots) și Intercepting Routes ((.)).
Cum arată structura folderelor
Ideea e simplă dar puțin contraintuitivă la început. Ai nevoie de un slot paralel (notat cu @) și de o rută interceptată (notată cu (.) pentru același nivel de folder).
Structura minimă în app/ arată cam așa:
app/
├── layout.tsx
├── page.tsx
├── photos/
│ └── [id]/
│ └── page.tsx
└── @modal/
├── (.)photos/
│ └── [id]/
│ └── page.tsx
└── default.tsx
Când navighezi prin Link de pe / către /photos/42, Next.js prinde tranziția pe client. În loc să te trimită pe ruta fizică app/photos/[id]/page.tsx, declanșează ruta interceptată din @modal/(.)photos/[id]/page.tsx. Pagina principală rămâne vizibilă sub slot-ul @modal.
Capcana de care te lovești sigur: default.tsx
Aici se încurcă 80% din devii care încearcă asta prima dată. Slot-urile paralele au nevoie neapărat de un fișier default.tsx. Când utilizatorul e pe ruta principală (/) și nu există niciun modal deschis, Next.js trebuie să știe ce să randeze în slot-ul @modal.
Dacă nu pui default.tsx care să returneze null, o să-ți iei 404 sau erori ciudate de hidratare la un simplu soft-navigation. Am pierdut vreo două ore la primul proiect căutând de ce îmi crapă build-ul pe Vercel din cauza asta.
Trade-off-uri reale
Arată spectaculos pentru UX și economisești cam 300ms la redarea modalului pentru că nu mai reîncarci tot layout-ul root. Totuși, vin și părțile mai puțin plăcute:
- Complexitate vizuală în proiect: Structura de directoare devine stufosă repede. Dacă ai 3 modale de genul ăsta, folderul
appîncepe să arate ca un puzzle. - State management la închidere: Ca să închizi modalul, nu e de ajuns să schimbi un state local
isOpen=false. Trebuie să facirouter.back()sau unrouter.push()către pagina părinte ca să cureți slot-ul. - Caching amestecat: Dacă faci fetch la date pe server în
photos/[id]/page.tsxși separat în(.)photos/[id]/page.tsx, asigură-te că folosești aceleași tag-uri de cache, altfel vezi date inconsecvente între modal și pagina standalone.
Nu folosiți abordarea asta pentru modale simple de tip "Termeni și Condiții" sau confirmări de ștergere. E proiectată strict pentru resurse care merită URL dedicat, indexabil și shareable.
Voi cum gestionați modalele cu URL deep-linkable în producție, mergeți pe App Router nativ sau tot pe query params (?modal=photo-123)?