"use client";
import { useContext, useRef } from "react";
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
export function FrozenRoute({ children }: { children: React.ReactNode }) {
const context = useContext(LayoutRouterContext);
const frozen = useRef(context);
return (
<LayoutRouterContext.Provider value={frozen.current}>
{children}
</LayoutRouterContext.Provider>
);
}Am trecut recent un proiect destul de măricel (pe la vreo 15k pagini indexate) pe Next.js App Router și, evident, clientul a vrut animații fluide de tranziție între pagini. Sună simplu la prima vedere. Pui AnimatePresence în layout, arunci un motion.div în pagină și gata. Ei bine, m-am lovit de o problemă destul de mare: în App Router, animațiile de exit pur și simplu nu rulează.
Am pierdut vreo două nopți încercând diverse hack-uri de pe GitHub până am înțeles exact de ce se întâmplă asta și cum se poate rezolva elegant.
De ce se rupe AnimatePresence în App Router?
În vechiul Pages Router, lucrurile erau directe. Puneai AnimatePresence în _app.js și foloseai router.route ca cheie (key). React știa că se schimbă ruta, dar Framer Motion apuca să rețină nodul vechi în DOM pentru a rula animația de ieșire.
În App Router, layout-urile sunt persistente, iar paginile sunt demontate extrem de agresiv. Când dai click pe un link, Next.js înlocuiește instantaneu nodul din DOM cu noua pagină. Pentru Framer Motion, elementul vechi a dispărut deja fizic din pagină înainte ca el să poată rula animația de exit. Nu are ce să mai animeze.
Soluția: Înghețarea rutei (Frozen Route)
Ca să forțăm React să păstreze temporar vechiul nod în DOM până când Framer Motion își termină treaba, trebuie să interceptăm contextul de routing din Next.js. Soluția pe care am implementat-o folosește un mic wrapper care „îngheață” contextul rutei curente.
Ideea este să salvăm contextul rutei într-un ref și să-l oferim mai departe prin providerul intern al Next.js (LayoutRouterContext). Astfel, chiar dacă Next.js încearcă să schimbe pagina, wrapper-ul nostru va continua să randeze vechea pagină pe durata animației de exit.
Trade-off-uri pe care trebuie să le accepți
Nimic nu e gratis în web dev, iar pattern-ul ăsta vine cu câteva chestii destul de spinoase de care trebuie să fii conștient:
- Dependența de API-uri interne: Soluția importă
LayoutRouterContextdinnext/dist/shared/lib/app-router-context.shared-runtime. Nu este un API public stabil. La un update major de Next.js s-ar putea să se schimbe calea importului sau structura contextului și să îți crape build-ul. - Scroll restoration dat peste cap: Dacă utilizatorul dă scroll pe o pagină lungă și apoi navighează, scroll-ul s-ar putea să sară brusc sus înainte ca animația de exit să se termine. Eu am rezolvat asta cu un
window.scrollTo(0, 0)declanșat manual abia pe evenimentul deonAnimationCompleteîn componenta cu animația. - Performanță pe pagini grele: Dacă noua pagină face fetch-uri masive direct în Server Components, tranziția poate avea un lag destul de vizibil (jank). Am economisit cam 30% din timpul de randare optimizând imaginile și făcând prefetch inteligent pe link-uri.
Voi cum ați rezolvat tranzițiile de pagină pe App Router? Ați mers pe varianta asta cu context freeze sau ați preferat să folosiți Template-uri simple și să renunțați complet la animațiile de exit?