Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Exit animation with Next.js #1375

Closed
MatteoGauthier opened this issue Dec 1, 2021 · 57 comments
Closed

[BUG] Exit animation with Next.js #1375

MatteoGauthier opened this issue Dec 1, 2021 · 57 comments
Labels
bug Something isn't working wontfix This will not be worked on

Comments

@MatteoGauthier
Copy link

1. Read the FAQs 👇

2. Describe the bug

I tried to integrate framer motion to next.js, I have components that appear on every page and when the road changes there is an animation.

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

https://codesandbox.io/s/github/MatteoGauthier/vertical-gallery/tree/52627524a54ee628bbde2f360c77b6d75c41593e

5. Expected behavior

Exit animation on route change

6. Video or screenshots

Enregistrement.de.l.ecran.2021-12-02.a.00.38.58.mov

Thanks

@MatteoGauthier MatteoGauthier added the bug Something isn't working label Dec 1, 2021
@MarcGuiselin
Copy link

In _app.tsx, you need to wrap your page component with an AnimatePresence like so:

<AnimatePresence exitBeforeEnter>
  <Component {...pageProps} key={router.pathname} />
</AnimatePresence>

See: https://wallis.dev/blog/nextjs-page-transitions-with-framer-motion

This used to work well with older versions of next.js, but doesn't work anymore. I can't figure out why.

@teauxfu
Copy link

teauxfu commented Dec 31, 2021

I struggled to get this working as well. However, I was able to get it to work after correctly with Next v12.0.7 specifying a key for the component as suggested by @MarcGuiselin.

export default function App({ Component, pageProps, router }) {
  return (
    <AnimatePresence exitBeforeEnter>
      <Component {...pageProps} key={router.pathname} />
    </AnimatePresence>
  );
}

https://www.framer.com/docs/animate-presence/##animating-custom-components

I'm relived that all my nested staggerChildren transitions in children seem to still be working! 👍🏻

@marcospassos
Copy link

Same issue here =/

@MagicMikeChen
Copy link

MagicMikeChen commented Jan 12, 2022

Thanks a lot, it fixed the issue.
When I wrap at the top level and include other HOC it won't work, need to wrapper the Component exact the parent level like this.

<AnimatePresence exitBeforeEnter> <Component {...pageProps} key={router.pathname} /> </AnimatePresence>

@marcospassos
Copy link

It doesn't work for me on Next's latest version

@john-rock
Copy link

john-rock commented Jan 19, 2022

@teauxfu solution resolved this for me on 12.0.8.

<Header />
  <AnimatePresence
        exitBeforeEnter
        initial={false}
        onExitComplete={() => window.scrollTo(0, 0)}
       >
          <Component {...pageProps} key={router.pathname} />
  </AnimatePresence>
<Footer />

@marcospassos
Copy link

marcospassos commented Jan 19, 2022

Ok, it's an issue with React 18 (concurrent mode), see #1421

@mahdisoultana
Copy link

mahdisoultana commented Apr 10, 2022

you can refer to this example from official documentation of Nextjs

https://github.com/vercel/next.js/blob/canary/examples/with-framer-motion/pages/_app.js

lately, i don't know why animation presence don't work like expected in the initial animation?

@aluku7-wq
Copy link

the exit animation is not working in dynamic routes

@beamercola
Copy link

@aluku7-wq Working for me, but be sure to pass the key as router.asPath, otherwise it's "/articles/[slug]"

<Component {...pageProps} key={router.asPath} />

@cristobalbahe
Copy link

cristobalbahe commented Jul 1, 2022

Hello! I have the same problem but it only happens in some transitions. I have (an example) the following routes:
/projects.js
/journal.js
/[....slug].js ---> /studio, /legal...

I have my app.js as the comment above, wrapped in LazyMotion with domAnimation and AnimatePresence:

 <LazyMotion features={domAnimation}>
   <AnimatePresence
        exitBeforeEnter
        onExitComplete={() => {
              console.log("EXIT COMPLETE", router.asPath);
          }}
    >
             <Component {...pageProps} key={router.asPath} />
    </AnimatePresence>
</LazyMotion>

When I navigate from /projects.js to /journal.js onExitComplete runs, but when I try to navigate from any of those two pages to one of [...slug.js] onExitComplete does not run.

Still, when I navigate between pages from [...slug.js] (from /studio to /legal) the transition works, so I am quite confused as to why this is happening.

Since my app is pretty complex by now (localized routes, caches, styled components) I don't know how I could make a sandbox to give an example, but maybe someone has any idea of what is happening.

Thank you all!

@cristobalbahe
Copy link

Okay, I found the cause. I had my _app.js main component wrapped in appWithTranslation HOC from next-i18next and this broke the transitions.

Do any of you guys have any idea how to fix this?

@f4z3k4s
Copy link

f4z3k4s commented Jul 22, 2022

@cristobalbahe did you find any solution for that? I am experiencing the same.

@cristobalbahe
Copy link

@cristobalbahe did you find any solution for that? I am experiencing the same.

Hey @f4z3k4s, I managed to fixed it by changing from next-i18next to next-intl. I guess since next-intl uses a regular component () instead of a HOC the context of framer doesn't get lost (I am not sure is because of this though). The thing is it works now :)

@f4z3k4s
Copy link

f4z3k4s commented Jul 26, 2022

@cristobalbahe Thanks for the answer. Since then, I've bypassed the issue by implementing a routing animation myself without AnimatePresence with the help of router events. Then, we know it's probably an issue with next-18next based on your solution.

@arkaydeus
Copy link

Done some extensive testing all options here. It's not isolated to next-i18next.

I DO not get the issue if I have a [id].tsx
I ALWAYS get the issue if I have [...slug].tsx

Something about the spread operator in the page name makes it happen.

@0xGar
Copy link

0xGar commented Nov 19, 2022

The solution from @teauxfu and @MarcGuiselin works flawlessly for me. Using React 18.1.0 & Next 12.1.6.

Thanks for the knowledge

@iamfrisbee
Copy link

iamfrisbee commented Nov 30, 2022

Having the same problem in nextjs 13.0.5 and react 18.0.2 with framer-motion 7.6.12; none of the previous solutions have been helpful. onExitComplete never triggers at all

@MattWIP
Copy link

MattWIP commented Dec 1, 2022

@iamfrisbee W/ Next 13.0.5 + React 18.2.0 + framer-motion 7.6.17 I was able to get page entrance & exit animations to work w/ this snippet:

    <AnimatePresence
      mode="wait"
      initial={false}
      onExitComplete={() => window.scrollTo(0, 0)}
    >
      <Component {...pageProps} key={router.pathname} />
    </AnimatePresence>

@tiendnm
Copy link

tiendnm commented Dec 10, 2022

@MattWIP

hi, have you tried with next13 app directory? I do the same but still not working

@iamfrisbee
Copy link

@iamfrisbee W/ Next 13.0.5 + React 18.2.0 + framer-motion 7.6.17 I was able to get page entrance & exit animations to work w/ this snippet:

    <AnimatePresence
      mode="wait"
      initial={false}
      onExitComplete={() => window.scrollTo(0, 0)}
    >
      <Component {...pageProps} key={router.pathname} />
    </AnimatePresence>

So I had to upgrade a few packages to get that, but no, it doesn't work. I verify this by putting a console.log in onExitComplete and it never runs.

@chipcullen
Copy link

I'm experiencing the same thing as @iamfrisbee - I tried many different ways yesterday, and none worked.

FWIW, commenters on this YouTube video also are running into the same thing.

@sebszocinski
Copy link

Yeah we've tried everything here and still nothing. We're on Next 13.1.1, React 18.2.0 and Framer Motion 8.0.2. Fingers crossed this gets fixed soon!

@joshdegouveia
Copy link

Running into this issue as well

@joshdegouveia
Copy link

joshdegouveia commented Jan 4, 2023

In case this isn't resolved soon and anyone else find this thread.. I found a hacky workaround for route animations thats compatible with the nextjs app directory feature.

const router = useRouter()
const controls = useAnimationControls()

const onRoute = useCallback((href: string) => async () => {
  await router.prefetch(href)
  await controls.start('exit')
  await router.push(href)
  await controls.set('hidden')
  await controls.start('enter')
}, [router, controls])
<motion.main
  animate={controls}
  variants={{
    hidden: { opacity: .3, x: -200, y: 0 },
    enter: { opacity: 1, x: 0, y: 0 },
    exit: { opacity: .3, x: 0, y: -100 },
  }}
  transition={{ type: 'keyframes', duration: 2 }}>
   {children}
</motion.main>
<button onClick={onRoute('page-1')}>
  Link
</button>

You can manually trigger enter/exit animations via the useAnimationControls hook. Instead of using <Link /> I call the onRoute method and route once the exit animation has completed.

NOTE: this code only works in layout.tsx components

@emilienbidet
Copy link

Next: 13.1.1
framer-motion: 8.4.3
using the app directory

Same issue. I will wait without exit animation until this is resolve.

@omerfe
Copy link

omerfe commented Jan 18, 2023

So i've faced the same issue using next^12.1.0 & react^18.2.0 & react-dom^18.2.0 & framer-motion^7.6.4
Turns out when you wrap your Component in _app.js like this:

<AnimatePresence
       onExitComplete={() =>
          console.log("exit completed, pathname:", router.asPath)
       }
       mode="wait"
       initial={false}
>
        <Component {...pageProps} key={router.asPath}  />
</AnimatePresence> 

It works fine on static pages that catches multiple routes like [[...slug.js]], but struggles to remove the elements from the dom if its a regular [...slug.js].

Couldn't find a proper way to handle this but instead of wrapping the Component in _app.js, I wrapped the layouts in [[...slug.js]] files fixed the issue.

Done some extensive testing all options here. It's not isolated to next-i18next.

I DO not get the issue if I have a [id].tsx I ALWAYS get the issue if I have [...slug].tsx

Something about the spread operator in the page name makes it happen.

@davidkhierl
Copy link

have the same issue, it also stated on the beta docs from nextjs to use templates.

I can see on react-devtools the key is updating but the exit animation is not working

@eddsaura
Copy link

Mine is not working either, but on normal components, not even pages.

@hasolu
Copy link

hasolu commented Mar 4, 2023

This work for me on _app.tsx

next 13.1.6
framer-motion 8.5.5

import { usePathname } from "next/navigation";

const pathname = usePathname();
    <AnimatePresence
          mode="wait"
          onExitComplete={doSomething}
        >
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.6, ease: "easeInOut" }}
            key={pathname}
          >
            <Component {...pageProps} />
          </motion.div>
        </AnimatePresence>

@davidkhierl
Copy link

@hasolu everything works fine using the pages except using the new app dir and the layout template

@hasolu
Copy link

hasolu commented Mar 4, 2023

@davidkhierl now i realize this code is not working in production, i'm dealing with this.

@harshhhdev
Copy link

harshhhdev commented Mar 15, 2023

I'm having issues with this too. React 18.2, Framer Motion 10.0.1 and Next.js 13.2.3
Have y'all found any workarounds yet? I'm tried @joshdegouveia's solution but I kept getting Error: NextRouter was not mounted.

UPDATE: if you get that error, change next/router to next/navigation.
I got the workaround working, but I still can't use it with my navbar which is in the actual layout component just yet.
Also, seems like I'm getting the opposite error here. I can't make my intro animations work 😅

@davidkhierl
Copy link

The problem is the <AnimatePresence/> should not experience any rerender, its either you try to create a wrapper before the layout or wait nextjs to fix the template file, this is where you will put this component, but upon checking from react dev tools the template component somewhat create another wrapper to it children hence it cannot access the key from the motion componet which is required to let the exit animation to fire

@psoaresbj
Copy link

I am still fighting with this!!!

Not even in non-dynamic pages and not using the new app dir structure - I can't get the exit animation to trigger nor the onExitComplete callback to run! 😢

Dep versions:

  "next": "^13.2.4",
  "react": "^18.2.0",
  "framer-motion": "^10.9.2"

Anyone cracked this already?

@ShueiYang
Copy link

@psoaresbj The Exit Animation work in the page dir, did you give a try ?

@harshhhdev
Copy link

harshhhdev commented Apr 8, 2023

@ShueiYang not super helpful. We all know the exit animations work with the page directory—the entire point of this issue is that they don't work with Next.js 13's app directory.

@ShueiYang
Copy link

@harshhhdev my bad if i misunderstood when he said it's not working even when he don't use the new app dir, I am also fighting with the exit animation in the app dir that's why i am here.

@psoaresbj
Copy link

Yes, in page dir, everything works fine!

@graceyudhaaa
Copy link

appdir is officially stable, hope this solved soon

@seantai
Copy link

seantai commented May 10, 2023

no idea what needs to happen to fix this, but Ill be super happy when app directory works with exit animations

@zackdotcomputer
Copy link

I tracked down what I think is the issue with appdir - documented here vercel/next.js#49596

TLDR; Next is sticking an unkeyed component between the layout (which persists between pages) and template (which does not). Because this means your only choices are the parent of an unkeyed element or something that will be cleared away on page navigation, there's nowhere to put the AnimatePresence object to capture a page-exit for animation.

@Cuteappi
Copy link

Cuteappi commented May 10, 2023

Whaat?
Im having a hard time trying to figure out how to get it working in the page directory.
I use
"framer-motion": "^10.12.9",
"next": "13.4.1",
"react": "18.2.0"

index.jsx
import Starterpage from '@/components/Starterpage/Starterpage.jsx'
import Textani from '@/components/Starterpage/textani.jsx'
import Dots from '@/components/Starterpage/Dots.jsx'
import { motion } from 'framer-motion'
import { useRouter } from "next/router";

import Head from 'next/head'

export default function Welcome(props) {
const router = useRouter()

return (
    <>
        <motion.div
            key={router.route}
            initial="initialState"
            animate="animateState"
            exit="exitState"
            transition={{
                duration: 0.75,
            }}
            variants={{
                initialState: {
                    opacity: 0
                },
                animateState: {
                    opacity: 1
                },
                exitState: {
                    scale : 0
                }
            }}
            style={{width: '100%', minHeight: '100vh'}}
        >
            <Head>
                <title>Welcome</title>
                <meta name="description" content="Welcome To my portfolio page" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <Starterpage>
                <Textani word={props.randomWord} />
                <Dots numdots={3} />
            </Starterpage>
        </motion.div>
    </>
)

}

export async function getStaticProps(context) {
const wordList = ['Hello', 'Konnichiwa', 'Bonjour', 'Guten', 'Ciao', 'Ola', 'Marhaba', 'Nǐn hǎo']

const randomWord = wordList[Math.floor(Math.random() * wordList.length)]

return {
    props: { randomWord }
};

}

__app.js
import '@/styles/globals.scss'
import { AnimatePresence } from 'framer-motion'

export default function App({ Component, pageProps }) {
return (
<AnimatePresence mode='wait' initial={false} onExitComplete={()=>{console.log('exit completed')}}>
<Component {...pageProps} />

)
}

/home.jsx
import Head from 'next/head'
import Homepage from '@/components/Homepage/Homepage'
import { motion } from 'framer-motion'
import { useRouter } from "next/router";

export default function Home() {
const router = useRouter()

return (
    <>
        <motion.div
            key={router.route}
            initial="initialState"
            animate="animateState"
            exit="exitState"
            transition={{
                duration: 0.75,
            }}
            variants={{
                initialState: {
                opacity: 0,
                clipPath: "polygon(0 0, 100% 0, 100% 100%, 0% 100%)",
                },
                animateState: {
                opacity: 1,
                clipPath: "polygon(0 0, 100% 0, 100% 100%, 0% 100%)",
                },
                exitState: {
                opacity: 0,
                clipPath: "polygon(50% 0, 50% 0, 50% 100%, 50% 100%)",
                },
            }}
            style={{width: '100%', minHeight: '100vh'}}
        >
            <Head>
                <title>Home</title>
                <meta name="description" content="Here you will some info about me" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <Homepage />
        </motion.div>
    </>
)

}

@rnnyrk
Copy link

rnnyrk commented Jul 21, 2023

I'm trying to make it work with the app directory as well, but can't seem to get an exit animation working properly.. Instead the elements are just removed without an animation. I thought the if statement within the AnimatePresence should trigger the exit animation? This component is in the root layout.tsx

// PageWrapper.tsx
'use client';

import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import { AnimatePresence, motion } from 'framer-motion';

export const PageWrapper = ({ children }: PageWrapperProps) => {
  const pathname = usePathname();
  const [isTransitioning, setIsTransitioning] = useState(false);

  useEffect(() => {
    setIsTransitioning(true);
    setTimeout(() => {
      setIsTransitioning(false);
    }, 2000);
  }, [pathname]);

  const variants = (index: number) => ({
    hidden: {
      scaleY: 0,
      transition: {
        duration: 0.2,
        delay: index * 0.1,
      },
    },
    visible: {
      scaleY: 1,
      transition: {
        duration: 0.2,
        delay: index * 0.1,
      },
    },
  });

  return (
    <AnimatePresence
      initial={false}
      onExitComplete={() => console.log('EXIT COMPLETE')}
    >
      {isTransitioning ? (
        <>
          <motion.div
            key={`${pathname}_animation_1`}
            variants={variants(1)}
            initial="hidden"
            animate="visible"
            exit="hidden"
            className="w-[50vw] h-[100vh] bg-rnny-primary fixed bottom-0 left-0 z-50"
          />
          <motion.div
            key={`${pathname}_animation_2`}
            variants={variants(2)}
            initial="hidden"
            animate="visible"
            exit="hidden"
            className="w-[50vw] h-[100vh] bg-rnny-primary-tint fixed bottom-0 left-[50vw] z-50"
          />
        </>
      ) : null}
      {children}
    </AnimatePresence>
  );
};

What am I missing or not understanding?

EDIT:
Nvm, found it; Had to wrap my motion components in a <Fragment key="loading_animator"> with a key, so "Framer Motion can track the AnimatePresence" direct child presence.

@dnlaviv
Copy link

dnlaviv commented Jul 22, 2023

@rnnyrk can you provide a code example of how you solved it?

@rnnyrk
Copy link

rnnyrk commented Jul 24, 2023

@dnlaviv For full code see my repo

// layout.tsx
import './global.css';
import type * as i from 'types';

import { PageWrapper } from 'modules/layouts/PageWrapper';

const Layout = ({ children }: Props) => {
  return (
    <html lang="en">
      <head />
      <body>
        <main>
          <PageWrapper>{children}</PageWrapper>
        </main>
      </body>
    </html>
  );
};

type Props = i.NextPageProps<{
  children: React.ReactNode;
}>;

export default Layout;
// PageWrapper.tsx
'use client';

import { Fragment, useEffect, useRef, useState } from 'react';
import { usePathname } from 'next/navigation';
import { AnimatePresence, motion, Variants } from 'framer-motion';

import { cn } from 'utils';

export const PageWrapper = ({ children }: PageWrapperProps) => {
  const pathname = usePathname();
  const [isTransitioning, setIsTransitioning] = useState(true);
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    setIsTransitioning(true);
    timeoutRef.current = setTimeout(() => {
      setIsTransitioning(false);
      window.scrollTo(0, 0);
    }, 1100);

    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [pathname]);

  const variants: (index: number) => Variants = (index: number) => ({
    hidden: {
      scaleY: 0,
      transition: {
        duration: 0.4,
        delay: index * 0.05,
        ease: 'easeInOut',
      },
    },
    visible: {
      scaleY: 1,
      transition: {
        duration: 0.4,
        delay: index * 0.05,
        ease: 'easeInOut',
      },
    },
  });

  return (
    <AnimatePresence>
      {isTransitioning ? (
        <Fragment key="route_transition_animator">
          {[...Array(4).keys()].map((index) => {
            return (
              <motion.div
                key={`${pathname}_animation_${index}`}
                variants={variants(index)}
                initial="hidden"
                animate="visible"
                exit="hidden"
              />
            );
          })}
        </Fragment>
      ) : null}
      {isTransitioning ? <div className="min-w-screen min-h-screen" /> : children}
    </AnimatePresence>
  );
};

type PageWrapperProps = {
  children: React.ReactNode;
};

@gimwachan-git
Copy link

The exit animation seems to be determined by whether the element is unmounted or not. And AnimatePresence is too complex and it seems that the timing of the exit animation can't be customized, so I wrote a component to control when the motion animations are executed.
Although it's not perfect, I'm going to use it in a production environment.
It is an example of how it's used in CodeSandbox.

@colonder
Copy link

@rnnyrk Please, don't mind my question as I'm not a web dev per se - how does your solution relate to SSR? I guess when you wrap all your components in a component with use client at the top, SSR is basically impossible, and this in turn impacts SEO.

@rnnyrk
Copy link

rnnyrk commented Sep 11, 2023

@colonder This solution indeed doesnt support SSR. But there shouldn't be any hydration errors, because it's just an visual element on top of the current page. Next page loaded in the background. Didn't find a better solution yet. Open for one tho

@andrii-petlovanyi
Copy link

@colonder This solution indeed doesnt support SSR. But there shouldn't be any hydration errors, because it's just an visual element on top of the current page. Next page loaded in the background. Didn't find a better solution yet. Open for one tho

losing the SSR of the entire app at once is not the best sacrifice for the sake of page transition animation. + on pages, the animation did not break the entire SSR. I hope that in the near future there will be a solution from the side of the developers

@Gaurav4604
Copy link

#1850 (comment)
Hi everyone,
Please follow this, it has a potential hacky yet simple solution.

@alessiofrittoli
Copy link

alessiofrittoli commented Oct 29, 2023

Hi everyone,
For those who are still struggling with Framer Motion exit page animation in Next.js with the App Router I found and tested a nicer, cleaner, and more affordable solution than using onClick, onMouseEnter event listener on a custom <Link /> Component.

  • Tested with Next.js 14.0.0.

Hope this will help someone.

// src/app/layout.tsx

import PageAnimatePresence from 'components/HOC/PageAnimations/PageAnimatePresence'

const RootLayout: React.FC<React.PropsWithChildren> = ( { children } ) => (
	<html lang='en'>
		<body>
			{/* <SiteHeader /> */}
			<PageAnimatePresence>
				{ children }
			</PageAnimatePresence>
			{/* <SiteFooter /> */}
		</body>
	</html>
)

export default RootLayout
// src/components/HOC/PageAnimations/PageAnimatePresence/index.tsx

'use client'

import { usePathname } from 'next/navigation'
import { AnimatePresence, motion } from 'framer-motion'
import FrozenRoute from './FrozenRoute'

const PageAnimatePresence: React.FC<React.PropsWithChildren> = ( { children } ) => {

	const pathname = usePathname()

	return (
		<AnimatePresence mode='wait'>
			{/**
			 * We use `motion.div` as the first child of `<AnimatePresence />` Component so we can specify page animations at the page level.
			 * The `motion.div` Component gets re-evaluated when the `key` prop updates, triggering the animation's lifecycles.
			 * During this re-evaluation, the `<FrozenRoute />` Component also gets updated with the new route components.
			 */}
			<motion.div key={ pathname }>
				<FrozenRoute>
					{ children }
				</FrozenRoute>
			</motion.div>
		</AnimatePresence>
	)

}

export default PageAnimatePresence
// src/components/HOC/PageAnimations/PageAnimatePresence/FrozenRoute.tsx

'use client'

import { useContext, useRef } from 'react'
import { LayoutRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'

const FrozenRoute: React.FC<React.PropsWithChildren> = ( { children } ) => {

	const context	= useContext( LayoutRouterContext )
	const frozen	= useRef( context ).current

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

}

export default FrozenRoute
// src/components/HOC/PageAnimations/PageFadeInOut.tsx

'use client'

import { motion, type HTMLMotionProps, type Variants } from 'framer-motion'

const fadeInOut: Variants = {
	initial: {
		opacity: 0,
		pointerEvents: 'none',
	},
	animate: {
		opacity: 1,
		pointerEvents: 'all',
	},
	exit: {
		opacity: 0,
		pointerEvents: 'none',
	},
}

const transition: HTMLMotionProps<'div'>[ 'transition' ] = {
	duration: 0.2,
	staggerChildren: 0.1,
}

const PageFadeInOut: React.FC<
	React.PropsWithChildren<HTMLMotionProps<'div'>>
> = ( props ) => (
	<motion.div
		initial='initial'
		animate='animate'
		exit='exit'
		variants={ fadeInOut }
		transition={ transition }
		{ ...props }
	/>
)

export default PageFadeInOut

And the page route:

// src/app/page.tsx

import PageFadeInOut from '@/components/HOC/PageAnimations/PageFadeInOut'

const HomePage: React.FC = ( props ) => (
	<PageFadeInOut>
		<h1>Home page</h1>
	</PageFadeInOut>
)

export default HomePage

@zachuri
Copy link

zachuri commented Nov 6, 2023

Hi @alessiofrittoli

I tried implementing your workaround and the animation on exit does work but I get these warnings.

By any chance do you get warning errors like this:

Warning: Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported.
    at FrozenRouter (webpack-internal:///(ssr)/./src/components/animation/frozen-route.tsx:15:70)
    at div
    at MotionComponent (webpack-internal:///(ssr)/./node_modules/.pnpm/framer-motion@10.16.4_react-dom@18.2.0_react@18.2.0/node_modules/framer-motion/dist/es/motion/index.mjs:49:65)
    at PresenceChild (webpack-internal:///(ssr)/./node_modules/.pnpm/framer-motion@10.16.4_react-dom@18.2.0_react@18.2.0/node_modules/framer-motion/dist/es/components/AnimatePresence/PresenceChild.mjs:15:26)
    at AnimatePresence (webpack-internal:///(ssr)/./node_modules/.pnpm/framer-motion@10.16.4_react-dom@18.2.0_react@18.2.0/node_modules/framer-motion/dist/es/components/AnimatePresence/index.mjs:72:28)
    at PageAnimatePresence (webpack-internal:///(ssr)/./src/components/animation/page-animate-presence.tsx:16:32)
    at Lazy
    at f (webpack-internal:///(ssr)/./node_modules/.pnpm/next-themes@0.2.1_next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next-themes/dist/index.module.js:8:597)
    at $ (webpack-internal:///(ssr)/./node_modules/.pnpm/next-themes@0.2.1_next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next-themes/dist/index.module.js:8:348)
    at ThemeProvider (webpack-internal:///(ssr)/./src/components/theme-proivder.tsx:13:26)
    at Lazy
    at body
    at html
    at RedirectErrorBoundary (webpack-internal:///(ssr)/./node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/components/redirect-boundary.js:71:9)
    at RedirectBoundary (webpack-internal:///(ssr)/./node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/components/redirect-boundary.js:79:11)
    at ReactDevOverlay (webpack-internal:///(ssr)/./node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(ssr)/./node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:298:11)
    at Router (webpack-internal:///(ssr)/./node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/components/app-router.js:154:11)
    at ErrorBoundaryHandler (webpack-internal:///(ssr)/./node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/components/error-boundary.js:99:9)
    at ErrorBoundary (webpack-internal:///(ssr)/./node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/components/error-boundary.js:128:11)
    at AppRouter (webpack-internal:///(ssr)/./node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/client/components/app-router.js:426:13)
    at Lazy
    at Lazy
    at /home/zachuri/Documents/Learn/projects/zachuri-portfolio/node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:374733
    at /home/zachuri/Documents/Learn/projects/zachuri-portfolio/node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:374733
    at ServerInsertedHTMLProvider (/home/zachuri/Documents/Learn/projects/zachuri-portfolio/node_modules/.pnpm/next@14.0.1_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:38:23140)

@alessiofrittoli
Copy link

alessiofrittoli commented Nov 6, 2023

Hi @zachuri,
I'm encountering these warnings when I run the project in development mode and editing component files.
No warnings in production envs.
So it's something related to runtime server rendering in dev envs.

@rnnyrk
Copy link

rnnyrk commented Nov 9, 2023

Not sure what the solution is, but i see the warning as well. Furthermore @alessiofrittoli i think your solution is the best one so far, so thanks for pointing it out! Although I think this issue might break HMR as well, not sure if that's the case for you guys as well?

@mattgperry mattgperry added the wontfix This will not be worked on label Jan 3, 2024
@mattgperry
Copy link
Collaborator

In some of these cases, children were not being changed, in some AnimatePresence itself is being removed/changed, neither are according to usage recommendations.

For whole page transitions, if it's a problem with client/server components, it's probably better to prefer the View Transitions API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests