eduardweb.
Animații (Framer Motion)Intermediar#nextjs#react#framer-motion#animatii

AnimatePresence în Next.js App Router: cum faci exit transitions fără hack-uri urâte

De Răzvan Matei, 12 oct. 2026 · 4 vizualizări · 2 like-uri

Postat acum 3 zile
typescript
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).current

  return (
    <LayoutRouterContext.Provider value={frozen}>
      {children}
    </LayoutRouterContext.Provider>
  )
}

Dacă ai migrat vreodată un proiect de la Pages Router la App Router, probabil primul lucru care s-a bușit vizual a fost tranziția între pagini. În Pages Router trânteai un <AnimatePresence mode="wait"> peste Component în _app.tsx, legai cheia de router.asPath și viața era frumoasă.

În App Router, treaba asta e moartă din start. Next.js descarcă și înlocuiește arborele de componente pe client instantaneu, ceea ce înseamnă că AnimatePresence nu mai apucă să ruleze animația de exit. Pagina veche dispare brusc, iar cea nouă intră frumos cu fade, lăsând un sentiment de glitch ieftin.

De ce template.tsx nu e răspunsul complet

Mulți zic: „folosește template.tsx, că se remontează la fiecare navigare!”. Corect, se remontează. Dar se remontează doar pentru intrare (enter). În clipa în care schimbi ruta, instanța veche este distrusă înainte ca Framer Motion să poată bloca unmount-ul din DOM.

La un proiect de e-commerce cu vreo 14k sesiuni pe zi, clientul ținea neapărat la un efect fluid de slide între categoriile de produse. Am pierdut o zi întreagă testând variante până când mi-am dat seama că singura soluție viabilă este să „îngheți” randarea rutei vechi cât timp rulează tranziția.

Pattern-ul Frozen Route

Ca să convingi App Router-ul să țină componentele vechi în viață pe durata animației, trebuie să interceptezi contextul intern de routing. Framer Motion are nevoie de timp fizic în DOM ca să animeze proprietățile definite în exit={{ opacity: 0 }}.

Trucul stă într-un wrapper client-side care folosește LayoutRouterContext din Next.js. Sună puțin periculos pentru că atingi API-uri interne, dar e singura metodă solidă până când echipa Next.js va oferi un suport nativ pentru exit transitions pe Server Components.

Ideea e simplă: când ruta se schimbă (pathname devine altul), componenta îngheață contextul vechi până când AnimatePresence semnalează callback-ul de finalizare (onExitComplete). Copiii rutei anterioare rămân vizibili și activi pe durata celor 200-300ms de animație.

Trade-off-uri reale: când să NU faci asta

Pattern-ul ăsta arată senzațional pe mockup-uri și funcționează perfect pe pagini statice sau de marketing. Dar vine cu costuri clare pe care trebuie să le cântărești:

  1. Scroll restoration devine un coșmar. Dacă userul dă scroll rapid și schimbă ruta, Next.js încearcă să reseteze scroll-ul la top în timp ce pagina veche încă iese din cadru. Rezultatul e un salt vizual deranjant dacă nu decuplezi manual scroll behavior-ul.
  2. Risc de memory leak pe rute grele. Dacă pagina pe care o părăsești are grafice Canvas, WebGL sau liste virtualizate imense, ținerea lor în memorie încă 400ms în paralel cu încărcarea rutei noi poate duce la frame drops vizibile pe telefoane mid-range.
  3. Dependența de internele Next.js. Dacă Vercel redenumește sau mută LayoutRouterContext într-un minor release, codul tău crapă la build.

Personal, folosesc pattern-ul ăsta exclusiv în flow-uri compacte: wizard-uri de onboarding, formulare multi-step sau galerii de portofoliu. Pe pagini cu mult conținut dinamic, un simplu fade-in subtil direct pe noua pagină (fără exit animation) e de zece ori mai sigur și scutește multe bătăi de cap.

Voi ați găsit altă metodă mai curată fără să atingeți contextul intern de router?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.