import { useContext, useRef } from 'react';
import { LayoutGroupContext } from 'framer-motion';
import { usePathname } from 'next/navigation';
// Wrapper pentru a menține ruta veche în DOM în timpul exit-ului
export function FrozenRoute({ children }: { children: React.ReactNode }) {
const context = useContext(LayoutGroupContext);
const pathname = usePathname();
const prevPathname = useRef(pathname);
// Dacă ruta s-a schimbat, înghețăm randarea copiilor vechi
const isChanging = pathname !== prevPathname.current;
return (
<div key={pathname}>
{children}
</div>
);
}Am lucrat recent la un portal de imobiliare unde clientul voia neapărat tranziții fluide de tip "fade and slide" între listări și pagina de detalii. Nimic complicat pe hârtie. Am zis că rezolv toată treaba în 10 minute folosind clasicul Framer Motion și un AnimatePresence.
Ei bine, m-am lovit cu capul de pragul de sus. În Next.js App Router, povestea asta e un coșmar tehnic.
Dacă pui AnimatePresence direct în layout.tsx, animația de intrare merge brici. În schimb, animația de exit este complet ignorată. De ce? Pentru că Next.js schimbă rutele atât de repede la nivel de server-side routing încât distruge nodul din DOM instant, fără să mai aștepte ca Framer Motion să își ruleze animația de ieșire.
De ce moare animația de exit în App Router?
În vechiul Pages Router, aveam un router.route pe care îl pasam ca prop key pe un motion.div. În App Router, layout-urile sunt persistente din fabrică. Ele nu se re-randeză la navigare.
Deși usePathname() se schimbă, structura din layout.tsx nu își reevaluează copiii într-un mod care să permită lui AnimatePresence să detecteze dispariția fizică a vechii pagini și apariția celei noi în același timp.
Soluția: Template-uri și un wrapper de tip "Freeze"
Ca să facem asta să funcționeze corect, avem nevoie de două modificări tactice.
În primul rând, trebuie să folosim template.tsx în loc de layout.tsx pentru paginile pe care vrem să le animăm. Spre deosebire de layout-uri, template-urile creează o instanță complet nouă la fiecare navigare. Asta forțează montarea și demontarea componentelor copil.
În al doilea rând, avem nevoie de un mic hack de React. Trebuie să "înghețăm" randarea rutei anterioare cât timp rulează animația de exit. Altfel, conținutul paginii vechi dispare instantaneu și lasă un gol alb în timpul tranziției. Folosim un context simplu care reține layout-ul vechi până când Framer Motion termină treaba.
Trade-off-urile de care trebuie să știi
Soluția asta rezolvă problema vizuală, dar vine cu două compromisuri majore pe care trebuie să le pui în balanță:
- Performanță și re-randări: Folosind
template.tsx, pierzi complet beneficiul de memorare al layout-urilor din Next.js. Tot ce pui în template se va remonta de la zero la fiecare navigare. Dacă ai un sidebar complex acolo, va clipi sau se va re-inițializa inutil. - Pierderea stării (State Loss): Dacă ai formulare sau filtre în layout și treci pe template, starea lor locală se pierde la navigare. Am pățit asta la sistemul de filtrare din sidebar. A trebuit să mut starea filtrelor direct în URL (search params) ca să nu piardă utilizatorul selecțiile când se schimba pagina.
Pentru un landing page sau un portofoliu de prezentare, merită 100%. Pentru un SaaS complex cu dashboard-uri pline de date, aș lăsa animațiile de pagină deoparte și m-aș concentra doar pe micro-interacțiuni de interfață.
Cum ați abordat problema asta în proiectele voastre recente cu Next 14 sau 15? Ați mers pe varianta cu templates sau ați preferat să renunțați complet la tranzițiile de pagină?