'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { usePathname } from 'next/navigation';
export default function Template({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
{children}
</motion.div>
</AnimatePresence>
);
}Am văzut zeci de thread-uri pe GitHub despre cum se rup animațiile de exit în Next.js App Router. Am trecut și eu prin asta la un proiect recent cu 15k utilizatori activi, unde designerul a insistat pe tranziții fluide între pagini. Dacă pui AnimatePresence în layout.tsx, pur și simplu nu merge.
După trei zile de înjurături și teste, m-am lămurit cum stă treaba. Hai să vedem de ce apare problema asta și cum o rezolvăm curat.
De ce moare exit animation în App Router?
În vechiul Pages Router, foloseam key={router.route} pe componenta de pagină. AnimatePresence știa când se schimbă cheia, ținea vechiul nod în DOM până rula animația de exit, apoi îl ștergea.
În App Router, arhitectura e complet diferită. layout.js persistă și nu se remontează la navigare. Iar când Next.js schimbă pagina, distruge nodul din DOM instantaneu. AnimatePresence nici nu apucă să clipească, pentru că elementul pe care voia să-l animeze nu mai există fizic în pagină.
Soluția rapidă: Folosește template.tsx în loc de layout.tsx
Mulți uită că Next.js oferă și template.tsx. Spre deosebire de layout-uri, template-urile creează o instanță nouă la fiecare navigare. Asta înseamnă că se montează și se demontează complet.
Dacă pui AnimatePresence și un motion.div cu o cheie unică în template.tsx, animația va merge perfect.
Dar atenție la marele trade-off: pierzi starea. Dacă ai un input în care userul scria ceva sau o stare locală de React în acel template, totul se resetează la schimbarea rutei. Pentru pagini simple de prezentare, e perfect. Pentru dashboard-uri complexe, e o mare problemă.
Cum legăm usePathname de AnimatePresence
Ca să forțăm Framer Motion să recunoască schimbarea de pagină ca pe un eveniment de unmount/mount, trebuie să-i dăm o cheie dinamică. Cel mai simplu mod este să folosim usePathname() din next/navigation ca și key pentru wrapper-ul nostru animat.
Uită-te la codul de mai jos. Acesta este un template.tsx de bază care rezolvă problema tranzițiilor de pagină.
Trade-off-ul de performanță
Pe lângă pierderea stării, mai e o chestie. La un proiect mare, remontarea completă a arborelui de componente din template poate genera un mic lag (un stutter de câteva milisecunde), mai ales pe telefoane mai vechi.
Dacă ai pagini foarte grele cu multe imagini, s-ar putea să vrei să animezi doar elemente specifice din pagină (cum ar fi hero section-ul) în loc să pui o tranziție pe tot body-ul.
Voi cum ați rezolvat tranzițiile de pagină în App Router? Ați mers pe varianta simplă cu template.tsx sau ați scris vreun provider custom care îngheață contextul rutei?