"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: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
>
{children}
</motion.div>
</AnimatePresence>
);
}Dacă ai trecut recent de la Pages Router la App Router în Next.js, probabil ai observat că animațiile de 'exit' cu Framer Motion au devenit subit un coșmar. Am pățit-o la un proiect de prezentare pentru un studio de arhitectură, unde clientul voia un feel de aplicație mobilă, cu fade-uri lungi între pagini, și m-am prins destul de târziu de ce AnimatePresence pur și simplu refuza să colaboreze.
Problema e fundamentală: App Router-ul e mult mai agresiv cu unmounting-ul componentelor. În vechiul _app.js, aveai un singur punct de intrare unde puteai wrap-ui totul și funcționa. În noua structură, dacă pui animația în layout.js, surpriză: layout-ul nu se randează din nou la navigare, deci animația de intrare nu pleacă, iar cea de ieșire nu are contextul necesar ca să ruleze înainte ca DOM-ul să fie șters.
Diferența critică dintre Layout și Template
Prima greșeală pe care am făcut-o a fost să forțez animația în layout.tsx. Layout-urile sunt persistente. Asta e bine pentru performanță, dar groaznic pentru animații. Soluția oficială, dar ignorată de mulți, este fișierul template.tsx.
Spre deosebire de un layout, un template creează o instanță nouă a componentelor de fiecare dată când schimbi ruta. Am testat asta pe un proiect cu vreo 12 pagini și, deși sună a overhead, impactul asupra performanței a fost neglijabil — am văzut o creștere de sub 20ms în scripting time, ceea ce e insesizabil pentru user, dar rezolvă complet problema trigger-ului pentru Framer Motion.
Trucul cu Pathname pentru AnimatePresence
Chiar și cu template-uri, AnimatePresence are nevoie de o cheie unică (key) ca să știe că 'vechea' pagină trebuie să ruleze animația de exit înainte să dispară. Dacă nu îi dai un key stabil și unic per rută, Framer Motion vede doar că s-a schimbat conținutul în interiorul aceluiași container și face swap-ul instant, fără tranziție.
Folosesc usePathname() din next/navigation ca să generez acest key. Atenție însă la trade-off: dacă ai pagini cu parametri (ex: /blog/[slug]), navigarea între două postări de blog va declanșa animația completă de pagină. Uneori e ce vrei, alteori e obositor. La proiectul de arhitectură de care ziceam, am decis să excludem anumite sub-rute din acest flow pentru că se simțea 'prea mult' la navigări rapide.
De ce mode="wait" e periculos
Toată lumea recomandă mode="wait" în AnimatePresence. E varianta 'curată' unde pagina veche dispare complet și abia apoi apare cea nouă. Dar am observat o chestie: pe conexiuni slabe (3G/4G instabil), dacă ai un exit animation de 500ms și apoi un fetch de date, userul stă în fața unui ecran gol secunde bune.
Am trecut pe mode="popLayout" și e mult mai fluid, dar necesită un pic de CSS magic (position absolute pe elementele care se animă) ca să nu sară layout-ul în sus și în jos când ambele pagini sunt prezente în DOM pentru scurt timp. Am economisit cam 300ms de 'perceived latency' doar făcând schimbarea asta.
Tu cum gestionezi tranzițiile astea? Mergi pe varianta simplă cu CSS transitions sau te complici cu template-uri pentru control total?