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

Cum faci AnimatePresence să meargă cu adevărat în Next.js App Router

De Ștefan Iliescu, 5 iun. 2026 · 3 vizualizări · 2 like-uri

Postat acum 4 zile
typescript
import { useContext, useRef, ReactNode, createContext } from "react";
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";

interface FrozenRouteProps {
  children: ReactNode;
}

export function FrozenRoute({ children }: FrozenRouteProps) {
  const context = useContext(LayoutRouterContext);
  const frozen = useRef(context);

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

Salutare! Dacă ai trecut recent de la Pages Router la App Router în Next.js și folosești Framer Motion, probabil ai observat rapid că animațiile de exit (exit={{ opacity: 0 }}) au încetat brusc să mai funcționeze pe pagini. E super frustrant și e o problemă extrem de comună.

Hai să vedem de ce se întâmplă asta și cum o rezolvăm fără să stricăm performanța aplicației.

De ce se rupe filmul?

În vechiul Pages Router, AnimatePresence funcționa destul de simplu la nivel de _app.tsx pentru că routerul trimitea componentul paginii vechi și pe cel al paginii noi în același timp pentru o scurtă perioadă.

În App Router, arhitectura e diferită. Layout-urile sunt persistente, iar paginile (componentele page.tsx) sunt demontate (unmounted) instantaneu de îndată ce ruta se schimbă. AnimatePresence pur și simplu nu apucă să își ruleze animația de exit pentru că elementul din DOM dispare înainte ca Framer Motion să poată interveni.

Am pățit asta la un proiect recent cu peste 10k useri activi, unde clientul voia neapărat tranziții fluide între pagini. Am pierdut câteva ore bune căutând o soluție curată care să nu implice hack-uri grosolane în layout.tsx.

Soluția: Pattern-ul Frozen Route

Ca să forțăm Next.js să păstreze vechea pagină în DOM până când se termină animația de exit, trebuie să "înghețăm" ruta curentă. Putem face asta prin definirea unui wrapper personalizat care salvează contextul rutei active.

Ideea e să folosim un LayoutContext custom în interiorul unui client component care învelește paginile noastre. Când ruta se schimbă, componentul nostru reține vechiul children până când Framer Motion semnalează că animația s-a terminat.

În codul de mai jos, am izolat acest comportament într-un provider pe care îl poți pune direct în layout.tsx sau într-un template de pagină.

Trade-off-uri de care trebuie să fii conștient

Nimic nu e gratis în web dev, mai ales când te bați cu ciclul de viață din React și Next.js:

  1. Performanța pe mobile: Cât timp rulează animația, ai practic două pagini randate concomitent în DOM. La un proiect cu layout-uri grele, am observat un drop de frame-uri pe telefoane mai vechi. Am rezolvat-o scurtând animațiile la maximum 250-300ms.
  2. Scroll Restoration: Next.js încearcă să schimbe poziția scroll-ului când ruta se schimbă, dar pagina ta veche e încă vizibilă. S-ar putea să ai nevoie să controlezi scroll-ul manual folosind window.scrollTo pe callback-ul onAnimationComplete din Framer Motion.

Pentru un site de prezentare sau un portofoliu, setup-ul ăsta merită din plin. Pentru aplicații SaaS complexe, cu tabele mari de date, aș evita tranzițiile pe pagină completă și m-as limita la animații micro în interiorul paginii.

Voi ce soluții ați găsit pentru tranzițiile de pagină în App Router? Mergeți pe varianta asta sau preferați să lăsați navigarea instantă?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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