johnpolacek / TweenPages

Build complex animated page transitions with GSAP and Next.js
tweenpages.vercel.app
MIT License
189 stars 32 forks source link

React 18 breaks outro animation #4

Open chimp1nski opened 2 years ago

chimp1nski commented 2 years ago

When using React 18 (18.2.0), at the end of the outro animation, the transitioned timeline element flashes briefly before routing to the next page and playing the intro animation.

https://user-images.githubusercontent.com/34653237/182190017-b79e69c0-c575-4b69-a278-37d862b14cc3.mov


Beware

This is not happening when using React 17 (17.0.1).

I've also deployed a production build as well as disabled react strict-mode in dev in order to see if it's the useEffect double firing that React 18 comes with. Although I might have overlooked something, I can rule this out for now.

Both React versions were tested with Next.js 12.2.3 and it seems that it's not a Next issue.


My guess is that there is a lack of cleanup functions in the useEffect hook and therefore weirdness happening but honestly I don't even know where to start debugging this.


I am not expecting you @johnpolacek to fix this, am just leaving this here for others that might experience the same weirdness. Thank you so much for this awesome guide on how to implement such complex animation stuff!

If I'll find a fix for this (other than reverting to React 17) I'll update this issue and create a PR.

Cheers

fmaillet24 commented 2 years ago

Same for me

thismarioperez commented 2 years ago

@fmaillet24 @chimp1nski @johnpolacek I've got a fix in place in my implementation. Turns out in my case, the comparison of children and displayChildren was always invalidated. To get around that, I memoized children, and everything seems to work fine for me now.

Here's my code for reference:

// lib
import {useContext, useMemo, useState } from "react";
import useIsomorphicLayoutEffect from "@/hooks/useIsomorphicLayoutEffect";
import { TransitionContext } from "@/context/TransitionContext";

export default function TransitionLayout({ children }) {
    const [displayChildren, setDisplayChildren] = useState(children);
    const memoizedChildren = useMemo(() => children, [children]);
    const { timeline, resetTimeline } = useContext(TransitionContext);

    useIsomorphicLayoutEffect(() => {
        if (memoizedChildren !== displayChildren) {
            // console.log("children not equal");
            if (timeline.duration() === 0) {
                // there are no outro animations, so immediately transition
                setDisplayChildren(children);
            } else {
                timeline.play().then(() => {
                    // console.log("page transition played");
                    // outro complete so reset to an empty paused timeline
                    resetTimeline();
                    setDisplayChildren(children);
                });
            }
        }
    }, [memoizedChildren]);

    return <div>{displayChildren}</div>;
}
kadekjayak commented 1 year ago

any updates on it??

the code from @thismarioperez doesn't works for me. to get around that, I add opacity: 0 and setTimeout before replacing the children.

const [displayChildren, setDisplayChildren] = useState(children);
const memoizedChildren = useMemo(() => children, [children]);
const { timeline } = useContext(TransitionContext);
const el = useRef<HTMLDivElement>(null);

useIsomorphicLayoutEffect(() => {
    if (memoizedChildren !== displayChildren) {
      if (timeline.duration() === 0) {
        // there are no outro animations, so immediately transition
        setDisplayChildren(children);
      } else {
        timeline.play().then(() => {
          // outro complete so reset to an empty paused timeline
          timeline.seek(0).pause().clear();

          if (el.current) {
            el.current.style.opacity = `0`;
          }

          /**
           * Avoid flashy
           */
          setTimeout(() => {
            setDisplayChildren(children);
            if (el.current) {
              el.current.style.opacity = `1`;
            }
          }, 200);
        });
      }
    }
  }, [memoizedChildren]);

  return (
    <div ref={el} style={{ opacity: 1 }}>
      {displayChildren}
    </div>
  );

I'm not sure if it's a good practice, but in my case blank white looks better than flashes..

gcolombi commented 1 year ago

@chimp1nski @fmaillet24 @kadekjayak

I found a way that works for me using next 13.1.2 and react 18.2.0. I used the router.asPath as condition/dependency in useIsomorphicLayoutEffect instead of the children prop because I don't want the animations to trigger if the current page link is clicked. To avoid the flash, I used timeline.pause().clear().

import useTransitionContext from '@/context/transitionContext';
import { useState } from 'react';
import { useRouter } from 'next/router';
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';

export default function TransitionLayout({
    children
}) {
    const router = useRouter();
    const [currentPage, setCurrentPage] = useState({
        route: router.asPath,
        children
    })
    const { timeline } = useTransitionContext();

    useIsomorphicLayoutEffect(() => {
        if (currentPage.route !== router.asPath) {
            if (timeline.duration() === 0) {
                /* There are no outro animations, so immediately transition */
                setCurrentPage({
                    route: router.asPath,
                    children
                })
            } else {
                timeline.play().then(() => {
                    /* outro complete so reset to an empty paused timeline */
                    timeline.pause().clear();
                    setCurrentPage({
                        route: router.asPath,
                        children
                    })
                })
            }
        }
    }, [router.asPath]);

    return (
        <div className='u-overflow--hidden'>
            {currentPage.children}
        </div>
    );
}