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

Framer Motion și AnimatePresence în App Router: Cum scoți exit animations funcționale

De Andreea Crăciun, 11 oct. 2026 · 7 vizualizări · 2 like-uri

Postat acum 5 zile
typescript
'use client';

import { useContext, useRef } from 'react';
import { LayoutRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime';
import { usePathname } from 'next/navigation';
import { AnimatePresence, motion } from 'framer-motion';

function FrozenRoute({ children }: { children: React.ReactNode }) {
  const context = useContext(LayoutRouterContext);
  const frozen = useRef(context).current;

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

export default function PageTransition({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();

  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        initial={{ opacity: 0, y: 12 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -12 }}
        transition={{ duration: 0.25, ease: 'easeInOut' }}
      >
        <FrozenRoute>{children}</FrozenRoute>
      </motion.div>
    </AnimatePresence>
  );
}

Tranzițiile fluide între pagini în App Router sunt o durere de cap dacă încerci să copiezi rețeta clasică din Pages Router. În vechiul _app.tsx, pasai Component cu un key={router.asPath} în AnimatePresence și totul mergea brici. În noul router din Next.js, când schimbi ruta, React Server Components descarcă noul arbore și îl aruncă pe cel vechi instant, fără să lase Framer Motion să-și ruleze varianta de exit.

Am dat cu capul de problema asta acum câteva luni pe un panou de gestiune cu vreo 15k utilizatori activi. Clientul voia neapărat slide-uri laterale fine între tab-urile principale. Rezultatul inițial? Pagina nouă intra frumos, dar cea veche dispărea brusc în 0 milisecunde, lăsând un ecran alb oribil pentru o fracțiune de secundă.

De ce template.tsx nu rezolvă problema

Prima reacție a multora e să sară pe template.tsx. Într-adevăr, Next.js remontează template.tsx la fiecare navigare, spre deosebire de layout.tsx care e persistent. E excelent dacă vrei o animație de intrare (initial -> animate).

Problema e la ieșire. template.tsx nu întârzie demontarea rutei curente. Când navighezi de la /dashboard la /settings, nodul vechi este eliminat din DOM imediat. AnimatePresence se uită după copii care pleacă, dar acei copii nu mai există în arborele React ca să poată fi animați. Ai nevoie de un mecanism care să înghețe contextul rutei anterioare până când animația de ieșire raportează că s-a terminat.

Soluția: Frozen Router pattern

Ca să convingi App Router-ul să țină nodul vechi pe ecran cât timp rulează exit, trebuie să interceptezi segmentul de rută și să "îngheți" children-ul folosind un context local.

Ideea e simplă: când pathname se schimbă, prinzi copiii vechi într-un wrapper care ignoră temporar noile actualizări de context din Next.js. Abia când Framer Motion termină callback-ul onExitComplete, permiți montarea conținutului rutei noi.

Am izolat logica într-o componentă de tip client pe care o pui direct în layout.tsx. Wrapper-ul de mai jos folosește LayoutRouterContext intern din Next.js pentru a păstra starea veche în viață pe durata tranziției.

Compromisul sincer: merită efortul?

Pattern-ul funcționează curat, dar vine cu niște costuri pe care trebuie să le cântărești:

  1. Scroll restoration devine haotic: Next.js încearcă să reseteze scroll-ul la (0, 0) în timp ce pagina veche încă face slide afară. Ești obligat să pui scroll={false} pe majoritatea componentelor <Link> și să controlezi manual scroll-ul după onExitComplete.
  2. Consum de memorie: Ții două pagini grele montate simultan timp de 300-400ms. Pe pagini cu tabele mari sau grafice SVG complexe, am văzut drop-uri de framerate de la 60fps la 35fps pe telefoane mid-range.
  3. Dependență de interne Next.js: Dacă te legi de LayoutRouterContext, există riscul ca un minor update de Next.js să schimbe signatura internă.

Pentru un landing page sau un dashboard cu pagini ușoare, efectul vizual e impecabil și merită cele câteva linii în plus. Pentru formulare masive sau date dense, o simplă animație de intrare cu CSS curat bate oricând un AnimatePresence forțat.

Voi ați rămas pe Framer Motion pentru navigări sau ați început deja să migrați către noul View Transitions API nativ din browser?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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