johnpolacek/TweenPages

React 18 breaks outro animation

chimp1nski opened this issue · 4 comments

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.

React-18-tweenpages-bug.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

Same for me

@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>;
}

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..

@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>
    );
}