next-page-transitions icon indicating copy to clipboard operation
next-page-transitions copied to clipboard

Is it possible to crossfade?

Open j2is opened this issue 4 years ago • 3 comments

All examples have one page shown at a time, is it possible to have the previous page visible while the new one transitions in? (Like the Instagram app)

j2is avatar Feb 29 '20 12:02 j2is

@j2is I managed to do this with https://github.com/reactjs/react-transition-group instead

mattjis avatar Nov 09 '20 04:11 mattjis

@mattjis Do you have an example by chance?

Cobertos avatar Sep 06 '21 12:09 Cobertos

import React, { useRef, useState, useEffect } from 'react';
import { CSSTransition } from 'react-transition-group';
import { useRouter } from 'next/router';
import './app.css';

function PageTransition(props) {
  const { c: PageComponent, lc: lastPageComponent } = props;
  const componentsDiffer = lastPageComponent &&
    PageComponent.key !== lastPageComponent.key;
  const [transitioning, setTransitioning] = useState(componentsDiffer); // Transition if components differ
  const [transitioningFor, setTransitioningFor] = useState(PageComponent.key); // Keep track of which we just transitioned in
  // If the components differ and PageComponent.key is new, we need to start
  // a new transition
  if (componentsDiffer &&
    transitioningFor !== PageComponent.key) {
    setTransitioningFor(PageComponent.key);
    setTransitioning(true);
  }

  function onFinishTransition() {
    console.log('FINISHED', PageComponent.key);
    setTransitioning(false);
  }
  function fixScroll() {
    // Scroll left to 0 every frame, firefox will fuck this up if not
    document.querySelector('.concerning').scrollLeft = 0;
  }

  console.log(PageComponent?.key, lastPageComponent?.key, transitioning, transitioningFor);
  return (
    <div className='concerning'>
      <CSSTransition in={!transitioning} appear={true} timeout={600} classNames="page-transition" exit={false} onExited={onFinishTransition}>
        <div>
          {PageComponent}
        </div>
      </CSSTransition>
      <CSSTransition in={transitioning} timeout={600} classNames="page-transition" enter={false} onExiting={fixScroll} onExited={onFinishTransition}>
        <div className="outgoing">
          {lastPageComponent}
        </div>
      </CSSTransition>
    </div>
  );
}

export default function MyApp(params) {
  const router = useRouter();
  const key = router.route;
  const { Component: PageComponent, pageProps } = params;
  // Needs to be in MyApp, otherwise it will get recreated with a new page when the page changes and then we lose the last component
  // If you useState, react throws lots of out of order hook errors, cant store JSX inside state
  const lastPageComponentRef = useRef(undefined); 

  const getLayoutAndPage = (PageComponent, pageProps) => {
    // TODO: Convert this into just one function
    // Use the layout defined at the page level, if available
    const getLayout = PageComponent.getLayout || ((page) => page);
    // Use the layout builder defined (which takes pageComponent and pageProps and builds the
    // pageComponent too
    const getLayoutBuilder = PageComponent.getLayoutBuilder || ((PageComponent, pageProps) => getLayout(<PageComponent {...pageProps} />));

    return (
      <div key={key}>
        {getLayoutBuilder(PageComponent, pageProps)}
      </div>
    );
  };

  const c = getLayoutAndPage(PageComponent, pageProps);
  const lc = lastPageComponentRef.current || getLayoutAndPage(PageComponent, pageProps);
  const ret = (
    <PageTransition lc={lastPageComponentRef.current} c={getLayoutAndPage(PageComponent, pageProps)} />
  );
  lastPageComponentRef.current = c;
  return ret;
}
.concerning {
  position: relative;
  width: 100%;
  overflow: hidden;
}

.page-transition-enter-done,
.page-transition-appear-done,
.page-transition-exit-done {
}
.page-transition-enter {
  transform: translateX(-100%);
}
.page-transition-enter-active {
  transform: translate(0);
  transition: transform 600ms;
  transition-timing-function: cubic-bezier(.26,1,.25,1);
}
.page-transition-exit {
  display: block !important;
  transform: translate(0);
}
.page-transition-exit-active {
  display: block !important;
  transform: translateX(100%);
  transition: transform 600ms;
  transition-timing-function: cubic-bezier(.26,1,.25,1);
}
.page-transition-exit-done {
  display: none;
}

.outgoing {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  pointer-events: none;
}

Rough code for anyone who needs it. This implements sliding pages in Next at the _app.js level. Things that I learned:

  • JSX can't be stored in useState, so you have to useRef to store a previous component
  • Need to have state for transitioning at top level App, otherwise it might recreate some of your state and you'll not be able to properly transition
  • Firefox will fuck up horizontal scrolling even when hidden so you have to set scrollLeft on some overflow:hidden container so that your page doesnt drift
  • You'll want one in transition, and one out, because you need to display 2 pages at once. One has exit disabled, the other enter disabled
  • Router specific styling will have issues with this, anywhere in the codebase (styles will change as the transition occurs, causing weird disjointness)
  • Getting the right variable for in was tough, because it needs to be true when the component changes up until the point that the transition ends. Then it needs to be flipped back so the next transition works (also why we disable exit and enter)

Cobertos avatar Sep 06 '21 21:09 Cobertos