'use client';
import { usePathname } from 'next/navigation';
import { AnimatePresence, motion } from 'framer-motion';
export default function PageTransitionLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.18, ease: 'easeInOut' }}
>
{children}
</motion.div>
</AnimatePresence>
);
}Dacă ai încercat vreodată să pui o animație de tranziție între pagini în Next.js App Router folosind Framer Motion, probabil ai văzut că initial și animate merg brici, dar exit e complet ignorat. Am pățit-o pe un dashboard cu 8k utilizatori zilnici, unde designerul insista pe un efect de slide-out la schimbarea paginilor din meniu.
De ce crapă AnimatePresence în noul router?
În vechiul Pages Router aveai acces la evenimentele globale din router (routeChangeStart, routeChangeComplete) și puteai amâna unmount-ul. În App Router, Next.js schimbă arborele de React asincron la nivel de Server Components. Când dai click pe o rută nouă, componenta veche este distrusă instantaneu din DOM înainte ca Framer Motion să apuce să-și declanșeze cadrul de exit.
AnimatePresence are nevoie ca copiii să rămână montați în React până când animația de ieșire se termină. Dacă React le dă unmount direct de sus, Framer Motion e complet neputincios.
Soluția curată: template.tsx în loc de layout.tsx
Prima greșeală pe care am făcut-o a fost să pun wrapper-ul în layout.tsx. Layout-urile din App Router nu se remontează când schimbi rutele copil — ăsta e tot scopul lor pentru performanță.
Trebuie să folosești fișierul template.tsx. Spre deosebire de layout, un template creează o instanță nouă de componentă și își resetează starea la fiecare navigare. Împreună cu usePathname() ca cheie unică, îi dai ocazia lui Framer Motion să detecteze că pagina veche a plecat.
Trade-off-urile reale din producție
Nimic nu vine gratis în App Router și e bine să știi la ce te înhami înainte să adaugi tranziții peste tot:
- Pierderea stării locale: Folosind
template.tsx, pierzi starea componentelor la orice navigare. Dacă ai un input de căutare sau un scroll position pe care voiai să le păstrezi intacte între tab-uri,template.tsxle va reseta de la zero. - Latența percepută: Dacă animația ta de exit durează 400ms, utilizatorul simte o latență artificială la schimbarea paginii. Pe un e-commerce cu mii de produse, o amânare de 400ms doar pentru vizual reduce direct rata de conversie.
- Hack-ul cu FrozenRouter: Există un pattern avansat unde interceptezi
LayoutRouterContextși îl "îngheți" pe durata animației. Funcționează vizual impecabil, dar la un upgrade minor de la Next.js 14.1 la 14.2 mi-a crăpat tot site-ul deoarece Vercel a modificat contextul intern nedocumentat.
Recomandarea mea după 2 ani de App Router: folosește animații doar pe micro-interacțiuni (modale, dropdown-uri, elemente din pagină). Dacă totuși aveți cerințe ferme de design pentru page transitions, soluția cu template.tsx și un fade rapid de 150ms este cea mai stabilă pe termen lung.
Voi cum ați rezolvat problema asta pe proiectele voastre? Ați mers pe template.tsx sau ați riscat cu contexte interne de React?