Javascript 反应路由器转换输入和输出事件

Javascript 反应路由器转换输入和输出事件,javascript,reactjs,react-router,transition,Javascript,Reactjs,React Router,Transition,我有一个相当基本的设置为一个小网站,我的工作。我正在使用React和React路由器4。现在,我想在用户进入一条路线时添加一个过渡,通过一些javascript动画在该路线中来回过渡。然而,我不知道如何正确地做到这一点?假设用户位于/并单击导航到/projects/one的链接,那么我如何才能为此启动转换,如果用户导航到该组件/路由,则如何启动转换?我不希望东西只是“卸载”,我希望它们在转换之间保持平稳,并拥有控制权。。? 超时值只是一个示例时间 目前,我有以下几点: 更新: 基于Ryan C代

我有一个相当基本的设置为一个小网站,我的工作。我正在使用React和React路由器4。现在,我想在用户进入一条路线时添加一个过渡,通过一些javascript动画在该路线中来回过渡。然而,我不知道如何正确地做到这一点?假设用户位于/并单击导航到/projects/one的链接,那么我如何才能为此启动转换,如果用户导航到该组件/路由,则如何启动转换?我不希望东西只是“卸载”,我希望它们在转换之间保持平稳,并拥有控制权。。? 超时值只是一个示例时间

目前,我有以下几点:

更新:

基于Ryan C代码示例,我已经能够提出一个非常接近我想要的解决方案,因此删除了我的旧代码,因为它离我最初的问题太远了

代码:

对于目前的版本,我有两个问题无法解决

  • 如果用户当前在主页(/)上,并且用户单击了同一路径的链接,我如何防止我的转换流发生,而什么都不做?同时不在浏览器中添加大量具有相同路径的历史记录

  • 如果用户位于主页(/)并导航到ProjectsPage(/projects/one),并且在转换完成之前,用户再次导航回主页(/),那么我希望主页的“transitionOut”停止,并再次运行“transitionIn”(有点像在两个页面之间倒带我的转换)。。也许它连接到1)


  • 我保留了这个答案,以便评论仍然有意义,并且可以看到变化,但这已经被

    以下是一些相关参考资料,我希望您已经了解其中一些:

    使用下面包含的代码,您可以快速看到效果

    下面的代码使用
    addEndListener
    属性在路径中使用Transition插入使用gsap的自定义动画。要使这项工作成功,有几个重要方面。为了使
    转换
    通过
    进入
    状态,在中的
    属性必须从
    false
    变为
    true
    。如果它从
    true
    开始,则它将立即跳到
    已输入的
    状态,而不进行转换。为了在
    路由
    中发生这种情况,您需要使用路由的
    子级
    属性(而不是
    组件
    渲染
    ),因为无论路由是否匹配,都将渲染子级。在下面的示例中,您将看到:

    <Route exact path="/projects/one">
        {({ match }) => <Projects show={match !== null} />}
    </Route>
    
    更新1:解决更新中的问题1。 react router版本4的一个优点是,路由可以出现在多个位置并控制页面的多个部分。在本文中,我对您的代码沙盒进行了更新,使主链接在链接和静态文本之间切换(尽管您可以将其更改为使用样式,使两者的外观相同)。我用LinkOrStaticText替换了链接(我做得很快,它可能需要一些改进来更有力地处理道具通过):

    const LinkOrStatic=props=>{
    const path=props.to;
    返回(
    {({match})=>{
    如果(匹配){
    返回道具。儿童;
    }
    返回(
    {props.children}
    );
    }}
    );
    };
    
    我将单独更新以解决问题2


    更新2:在试图解决问题2时,我发现了我在回答中使用的方法的一些基本问题。由于在某些情况下同时执行多条路由,以及由于正在进行的未安装的转换的奇怪残余而导致的问题,该行为变得混乱。我需要用一种不同的方法从头开始,所以我在另一个答案中发布了修改后的方法。

    因此,如果你从路线1切换到路线2,然后在路线1仍在退出时返回路线1,那么支持重新开始进入过渡的方法是相当棘手的。我所拥有的可能存在一些小问题,但我认为总体方法是合理的

    总体方法涉及从渲染路径(当前显示的可能处于转换状态的路径)中分离出目标路径(用户想要去的地方)。为了确保转换在适当的时间发生,状态用于逐步排序(例如,首先使用
    In=false
    呈现转换,然后使用
    In=true
    呈现进入的转换)。大部分复杂性在
    TransitionManager.js
    中处理

    我在代码中使用了钩子,因为我更容易在没有类语法开销的情况下处理逻辑,所以在接下来的几个月左右,这将只适用于alpha。如果官方版本中的钩子实现发生了任何改变,从而破坏了这段代码,我将在那时更新这个答案

    代码如下:

    index.js

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    
    import React from "react";
    import { BrowserRouter } from "react-router-dom";
    import LinkOrStatic from "./LinkOrStatic";
    import { componentInfoArray } from "./components";
    import {
      useTransitionContextState,
      TransitionContext
    } from "./TransitionContext";
    import TransitionRoute from "./TransitionRoute";
    
    const App = props => {
      const transitionContext = useTransitionContextState();
      return (
        <TransitionContext.Provider value={transitionContext}>
          <BrowserRouter>
            <div>
              <br />
              {componentInfoArray.map(compInfo => (
                <LinkOrStatic key={compInfo.path} to={compInfo.path}>
                  {compInfo.linkText}
                </LinkOrStatic>
              ))}
    
              {componentInfoArray.map(compInfo => (
                <TransitionRoute
                  key={compInfo.path}
                  path={compInfo.path}
                  exact
                  component={compInfo.component}
                />
              ))}
            </div>
          </BrowserRouter>
        </TransitionContext.Provider>
      );
    };
    export default App;
    
    import React, { useState } from "react";
    
    export const TransitionContext = React.createContext();
    export const useTransitionContextState = () => {
      // The path most recently requested by the user
      const [targetPath, setTargetPath] = useState(null);
      // The path currently rendered. If different than the target path,
      // then probably in the middle of a transition.
      const [renderInfo, setRenderInfo] = useState(null);
      const [exitTimelineAndDone, setExitTimelineAndDone] = useState({});
      const transitionContext = {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      };
      return transitionContext;
    };
    
    import React from "react";
    const Home = props => {
      return <div>Hello {props.state + " Home!"}</div>;
    };
    const ProjectOne = props => {
      return <div>Hello {props.state + " Project One!"}</div>;
    };
    const ProjectTwo = props => {
      return <div>Hello {props.state + " Project Two!"}</div>;
    };
    export const componentInfoArray = [
      {
        linkText: "Home",
        component: Home,
        path: "/"
      },
      {
        linkText: "Show project one",
        component: ProjectOne,
        path: "/projects/one"
      },
      {
        linkText: "Show project two",
        component: ProjectTwo,
        path: "/projects/two"
      }
    ];
    
    import React from "react";
    import { Route, Link } from "react-router-dom";
    
    const LinkOrStatic = props => {
      const path = props.to;
      return (
        <>
          <Route exact path={path}>
            {({ match }) => {
              if (match) {
                return props.children;
              }
              return (
                <Link className={props.className} to={props.to}>
                  {props.children}
                </Link>
              );
            }}
          </Route>
          <br />
        </>
      );
    };
    export default LinkOrStatic;
    
    import React from "react";
    import { Route } from "react-router-dom";
    import TransitionManager from "./TransitionManager";
    
    const TransitionRoute = props => {
      return (
        <Route path={props.path} exact>
          {({ match }) => {
            return (
              <TransitionManager
                key={props.path}
                path={props.path}
                component={props.component}
                match={match}
              />
            );
          }}
        </Route>
      );
    };
    export default TransitionRoute;
    
    import React, { useContext, useEffect } from "react";
    import { Transition } from "react-transition-group";
    import {
      slowFadeInAndDropFromAboveThenLeftRight,
      slowFadeOutAndDrop
    } from "./animations";
    import { TransitionContext } from "./TransitionContext";
    
    const NEW_TARGET = "NEW_TARGET";
    const NEW_TARGET_MATCHES_EXITING_PATH = "NEW_TARGET_MATCHES_EXITING_PATH";
    const FIRST_TARGET_NOT_RENDERED = "FIRST_TARGET_NOT_RENDERED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING";
    const TARGET_RENDERED = "TARGET_RENDERED";
    const NOT_TARGET_AND_NEED_TO_START_EXITING =
      "NOT_TARGET_AND_NEED_TO_START_EXITING";
    const NOT_TARGET_AND_EXITING = "NOT_TARGET_AND_EXITING";
    const NOT_TARGET = "NOT_TARGET";
    const usePathTransitionCase = (path, match) => {
      const {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      } = useContext(TransitionContext);
      let pathTransitionCase = null;
      if (match) {
        if (targetPath !== path) {
          if (
            renderInfo &&
            renderInfo.path === path &&
            renderInfo.transitionState === "exiting" &&
            exitTimelineAndDone.timeline
          ) {
            pathTransitionCase = NEW_TARGET_MATCHES_EXITING_PATH;
          } else {
            pathTransitionCase = NEW_TARGET;
          }
        } else if (renderInfo === null) {
          pathTransitionCase = FIRST_TARGET_NOT_RENDERED;
        } else if (renderInfo.path !== path) {
          if (renderInfo.transitionState === "exited") {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED;
          } else {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING;
          }
        } else {
          pathTransitionCase = TARGET_RENDERED;
        }
      } else {
        if (renderInfo !== null && renderInfo.path === path) {
          if (
            renderInfo.transitionState !== "exiting" &&
            renderInfo.transitionState !== "exited"
          ) {
            pathTransitionCase = NOT_TARGET_AND_NEED_TO_START_EXITING;
          } else {
            pathTransitionCase = NOT_TARGET_AND_EXITING;
          }
        } else {
          pathTransitionCase = NOT_TARGET;
        }
      }
      useEffect(() => {
        switch (pathTransitionCase) {
          case NEW_TARGET_MATCHES_EXITING_PATH:
            exitTimelineAndDone.timeline.kill();
            exitTimelineAndDone.done();
            setExitTimelineAndDone({});
            // Making it look like we exited some other path, in
            // order to restart the transition into this path.
            setRenderInfo({
              path: path + "-exited",
              transitionState: "exited"
            });
            setTargetPath(path);
            break;
          case NEW_TARGET:
            setTargetPath(path);
            break;
          case FIRST_TARGET_NOT_RENDERED:
            setRenderInfo({ path: path });
            break;
          case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
            setRenderInfo({ path: path, transitionState: "entering" });
            break;
          case NOT_TARGET_AND_NEED_TO_START_EXITING:
            setRenderInfo({ ...renderInfo, transitionState: "exiting" });
            break;
          // case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          // case NOT_TARGET:
          default:
          // no-op
        }
      });
      return {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      };
    };
    
    const TransitionManager = props => {
      const {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      } = usePathTransitionCase(props.path, props.match);
      const getEnterTransition = show => (
        <Transition
          key={props.path}
          addEndListener={slowFadeInAndDropFromAboveThenLeftRight()}
          in={show}
          unmountOnExit={true}
        >
          {state => {
            const Child = props.component;
            console.log(props.path + ": " + state);
            return <Child state={state} />;
          }}
        </Transition>
      );
      const getExitTransition = () => {
        return (
          <Transition
            key={props.path}
            addEndListener={slowFadeOutAndDrop(setExitTimelineAndDone)}
            in={false}
            onExited={() =>
              setRenderInfo({ ...renderInfo, transitionState: "exited" })
            }
            unmountOnExit={true}
          >
            {state => {
              const Child = props.component;
              console.log(props.path + ": " + state);
              return <Child state={state} />;
            }}
          </Transition>
        );
      };
      switch (pathTransitionCase) {
        case NEW_TARGET_MATCHES_EXITING_PATH:
        case NEW_TARGET:
        case FIRST_TARGET_NOT_RENDERED:
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          return null;
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
          return getEnterTransition(false);
        case TARGET_RENDERED:
          return getEnterTransition(true);
        case NOT_TARGET_AND_NEED_TO_START_EXITING:
        case NOT_TARGET_AND_EXITING:
          return getExitTransition();
        // case NOT_TARGET:
        default:
          return null;
      }
    };
    export default TransitionManager;
    
    import { TimelineMax } from "gsap";
    const startStyle = { autoAlpha: 0, y: -50 };
    export const slowFadeInAndDropFromAboveThenLeftRight = trackTimelineAndDone => (
      node,
      done
    ) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.set(node, startStyle);
      timeline
        .to(node, 0.5, {
          autoAlpha: 1,
          y: 0
        })
        .to(node, 0.5, { x: -25 })
        .to(node, 0.5, {
          x: 0,
          onComplete: done
        });
    };
    export const slowFadeOutAndDrop = trackTimelineAndDone => (node, done) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.to(node, 2, {
        autoAlpha: 0,
        y: 100,
        onComplete: done
      });
    };
    
    TransitionContext.js

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    
    import React from "react";
    import { BrowserRouter } from "react-router-dom";
    import LinkOrStatic from "./LinkOrStatic";
    import { componentInfoArray } from "./components";
    import {
      useTransitionContextState,
      TransitionContext
    } from "./TransitionContext";
    import TransitionRoute from "./TransitionRoute";
    
    const App = props => {
      const transitionContext = useTransitionContextState();
      return (
        <TransitionContext.Provider value={transitionContext}>
          <BrowserRouter>
            <div>
              <br />
              {componentInfoArray.map(compInfo => (
                <LinkOrStatic key={compInfo.path} to={compInfo.path}>
                  {compInfo.linkText}
                </LinkOrStatic>
              ))}
    
              {componentInfoArray.map(compInfo => (
                <TransitionRoute
                  key={compInfo.path}
                  path={compInfo.path}
                  exact
                  component={compInfo.component}
                />
              ))}
            </div>
          </BrowserRouter>
        </TransitionContext.Provider>
      );
    };
    export default App;
    
    import React, { useState } from "react";
    
    export const TransitionContext = React.createContext();
    export const useTransitionContextState = () => {
      // The path most recently requested by the user
      const [targetPath, setTargetPath] = useState(null);
      // The path currently rendered. If different than the target path,
      // then probably in the middle of a transition.
      const [renderInfo, setRenderInfo] = useState(null);
      const [exitTimelineAndDone, setExitTimelineAndDone] = useState({});
      const transitionContext = {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      };
      return transitionContext;
    };
    
    import React from "react";
    const Home = props => {
      return <div>Hello {props.state + " Home!"}</div>;
    };
    const ProjectOne = props => {
      return <div>Hello {props.state + " Project One!"}</div>;
    };
    const ProjectTwo = props => {
      return <div>Hello {props.state + " Project Two!"}</div>;
    };
    export const componentInfoArray = [
      {
        linkText: "Home",
        component: Home,
        path: "/"
      },
      {
        linkText: "Show project one",
        component: ProjectOne,
        path: "/projects/one"
      },
      {
        linkText: "Show project two",
        component: ProjectTwo,
        path: "/projects/two"
      }
    ];
    
    import React from "react";
    import { Route, Link } from "react-router-dom";
    
    const LinkOrStatic = props => {
      const path = props.to;
      return (
        <>
          <Route exact path={path}>
            {({ match }) => {
              if (match) {
                return props.children;
              }
              return (
                <Link className={props.className} to={props.to}>
                  {props.children}
                </Link>
              );
            }}
          </Route>
          <br />
        </>
      );
    };
    export default LinkOrStatic;
    
    import React from "react";
    import { Route } from "react-router-dom";
    import TransitionManager from "./TransitionManager";
    
    const TransitionRoute = props => {
      return (
        <Route path={props.path} exact>
          {({ match }) => {
            return (
              <TransitionManager
                key={props.path}
                path={props.path}
                component={props.component}
                match={match}
              />
            );
          }}
        </Route>
      );
    };
    export default TransitionRoute;
    
    import React, { useContext, useEffect } from "react";
    import { Transition } from "react-transition-group";
    import {
      slowFadeInAndDropFromAboveThenLeftRight,
      slowFadeOutAndDrop
    } from "./animations";
    import { TransitionContext } from "./TransitionContext";
    
    const NEW_TARGET = "NEW_TARGET";
    const NEW_TARGET_MATCHES_EXITING_PATH = "NEW_TARGET_MATCHES_EXITING_PATH";
    const FIRST_TARGET_NOT_RENDERED = "FIRST_TARGET_NOT_RENDERED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING";
    const TARGET_RENDERED = "TARGET_RENDERED";
    const NOT_TARGET_AND_NEED_TO_START_EXITING =
      "NOT_TARGET_AND_NEED_TO_START_EXITING";
    const NOT_TARGET_AND_EXITING = "NOT_TARGET_AND_EXITING";
    const NOT_TARGET = "NOT_TARGET";
    const usePathTransitionCase = (path, match) => {
      const {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      } = useContext(TransitionContext);
      let pathTransitionCase = null;
      if (match) {
        if (targetPath !== path) {
          if (
            renderInfo &&
            renderInfo.path === path &&
            renderInfo.transitionState === "exiting" &&
            exitTimelineAndDone.timeline
          ) {
            pathTransitionCase = NEW_TARGET_MATCHES_EXITING_PATH;
          } else {
            pathTransitionCase = NEW_TARGET;
          }
        } else if (renderInfo === null) {
          pathTransitionCase = FIRST_TARGET_NOT_RENDERED;
        } else if (renderInfo.path !== path) {
          if (renderInfo.transitionState === "exited") {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED;
          } else {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING;
          }
        } else {
          pathTransitionCase = TARGET_RENDERED;
        }
      } else {
        if (renderInfo !== null && renderInfo.path === path) {
          if (
            renderInfo.transitionState !== "exiting" &&
            renderInfo.transitionState !== "exited"
          ) {
            pathTransitionCase = NOT_TARGET_AND_NEED_TO_START_EXITING;
          } else {
            pathTransitionCase = NOT_TARGET_AND_EXITING;
          }
        } else {
          pathTransitionCase = NOT_TARGET;
        }
      }
      useEffect(() => {
        switch (pathTransitionCase) {
          case NEW_TARGET_MATCHES_EXITING_PATH:
            exitTimelineAndDone.timeline.kill();
            exitTimelineAndDone.done();
            setExitTimelineAndDone({});
            // Making it look like we exited some other path, in
            // order to restart the transition into this path.
            setRenderInfo({
              path: path + "-exited",
              transitionState: "exited"
            });
            setTargetPath(path);
            break;
          case NEW_TARGET:
            setTargetPath(path);
            break;
          case FIRST_TARGET_NOT_RENDERED:
            setRenderInfo({ path: path });
            break;
          case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
            setRenderInfo({ path: path, transitionState: "entering" });
            break;
          case NOT_TARGET_AND_NEED_TO_START_EXITING:
            setRenderInfo({ ...renderInfo, transitionState: "exiting" });
            break;
          // case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          // case NOT_TARGET:
          default:
          // no-op
        }
      });
      return {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      };
    };
    
    const TransitionManager = props => {
      const {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      } = usePathTransitionCase(props.path, props.match);
      const getEnterTransition = show => (
        <Transition
          key={props.path}
          addEndListener={slowFadeInAndDropFromAboveThenLeftRight()}
          in={show}
          unmountOnExit={true}
        >
          {state => {
            const Child = props.component;
            console.log(props.path + ": " + state);
            return <Child state={state} />;
          }}
        </Transition>
      );
      const getExitTransition = () => {
        return (
          <Transition
            key={props.path}
            addEndListener={slowFadeOutAndDrop(setExitTimelineAndDone)}
            in={false}
            onExited={() =>
              setRenderInfo({ ...renderInfo, transitionState: "exited" })
            }
            unmountOnExit={true}
          >
            {state => {
              const Child = props.component;
              console.log(props.path + ": " + state);
              return <Child state={state} />;
            }}
          </Transition>
        );
      };
      switch (pathTransitionCase) {
        case NEW_TARGET_MATCHES_EXITING_PATH:
        case NEW_TARGET:
        case FIRST_TARGET_NOT_RENDERED:
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          return null;
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
          return getEnterTransition(false);
        case TARGET_RENDERED:
          return getEnterTransition(true);
        case NOT_TARGET_AND_NEED_TO_START_EXITING:
        case NOT_TARGET_AND_EXITING:
          return getExitTransition();
        // case NOT_TARGET:
        default:
          return null;
      }
    };
    export default TransitionManager;
    
    import { TimelineMax } from "gsap";
    const startStyle = { autoAlpha: 0, y: -50 };
    export const slowFadeInAndDropFromAboveThenLeftRight = trackTimelineAndDone => (
      node,
      done
    ) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.set(node, startStyle);
      timeline
        .to(node, 0.5, {
          autoAlpha: 1,
          y: 0
        })
        .to(node, 0.5, { x: -25 })
        .to(node, 0.5, {
          x: 0,
          onComplete: done
        });
    };
    export const slowFadeOutAndDrop = trackTimelineAndDone => (node, done) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.to(node, 2, {
        autoAlpha: 0,
        y: 100,
        onComplete: done
      });
    };
    
    components.js

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    
    import React from "react";
    import { BrowserRouter } from "react-router-dom";
    import LinkOrStatic from "./LinkOrStatic";
    import { componentInfoArray } from "./components";
    import {
      useTransitionContextState,
      TransitionContext
    } from "./TransitionContext";
    import TransitionRoute from "./TransitionRoute";
    
    const App = props => {
      const transitionContext = useTransitionContextState();
      return (
        <TransitionContext.Provider value={transitionContext}>
          <BrowserRouter>
            <div>
              <br />
              {componentInfoArray.map(compInfo => (
                <LinkOrStatic key={compInfo.path} to={compInfo.path}>
                  {compInfo.linkText}
                </LinkOrStatic>
              ))}
    
              {componentInfoArray.map(compInfo => (
                <TransitionRoute
                  key={compInfo.path}
                  path={compInfo.path}
                  exact
                  component={compInfo.component}
                />
              ))}
            </div>
          </BrowserRouter>
        </TransitionContext.Provider>
      );
    };
    export default App;
    
    import React, { useState } from "react";
    
    export const TransitionContext = React.createContext();
    export const useTransitionContextState = () => {
      // The path most recently requested by the user
      const [targetPath, setTargetPath] = useState(null);
      // The path currently rendered. If different than the target path,
      // then probably in the middle of a transition.
      const [renderInfo, setRenderInfo] = useState(null);
      const [exitTimelineAndDone, setExitTimelineAndDone] = useState({});
      const transitionContext = {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      };
      return transitionContext;
    };
    
    import React from "react";
    const Home = props => {
      return <div>Hello {props.state + " Home!"}</div>;
    };
    const ProjectOne = props => {
      return <div>Hello {props.state + " Project One!"}</div>;
    };
    const ProjectTwo = props => {
      return <div>Hello {props.state + " Project Two!"}</div>;
    };
    export const componentInfoArray = [
      {
        linkText: "Home",
        component: Home,
        path: "/"
      },
      {
        linkText: "Show project one",
        component: ProjectOne,
        path: "/projects/one"
      },
      {
        linkText: "Show project two",
        component: ProjectTwo,
        path: "/projects/two"
      }
    ];
    
    import React from "react";
    import { Route, Link } from "react-router-dom";
    
    const LinkOrStatic = props => {
      const path = props.to;
      return (
        <>
          <Route exact path={path}>
            {({ match }) => {
              if (match) {
                return props.children;
              }
              return (
                <Link className={props.className} to={props.to}>
                  {props.children}
                </Link>
              );
            }}
          </Route>
          <br />
        </>
      );
    };
    export default LinkOrStatic;
    
    import React from "react";
    import { Route } from "react-router-dom";
    import TransitionManager from "./TransitionManager";
    
    const TransitionRoute = props => {
      return (
        <Route path={props.path} exact>
          {({ match }) => {
            return (
              <TransitionManager
                key={props.path}
                path={props.path}
                component={props.component}
                match={match}
              />
            );
          }}
        </Route>
      );
    };
    export default TransitionRoute;
    
    import React, { useContext, useEffect } from "react";
    import { Transition } from "react-transition-group";
    import {
      slowFadeInAndDropFromAboveThenLeftRight,
      slowFadeOutAndDrop
    } from "./animations";
    import { TransitionContext } from "./TransitionContext";
    
    const NEW_TARGET = "NEW_TARGET";
    const NEW_TARGET_MATCHES_EXITING_PATH = "NEW_TARGET_MATCHES_EXITING_PATH";
    const FIRST_TARGET_NOT_RENDERED = "FIRST_TARGET_NOT_RENDERED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING";
    const TARGET_RENDERED = "TARGET_RENDERED";
    const NOT_TARGET_AND_NEED_TO_START_EXITING =
      "NOT_TARGET_AND_NEED_TO_START_EXITING";
    const NOT_TARGET_AND_EXITING = "NOT_TARGET_AND_EXITING";
    const NOT_TARGET = "NOT_TARGET";
    const usePathTransitionCase = (path, match) => {
      const {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      } = useContext(TransitionContext);
      let pathTransitionCase = null;
      if (match) {
        if (targetPath !== path) {
          if (
            renderInfo &&
            renderInfo.path === path &&
            renderInfo.transitionState === "exiting" &&
            exitTimelineAndDone.timeline
          ) {
            pathTransitionCase = NEW_TARGET_MATCHES_EXITING_PATH;
          } else {
            pathTransitionCase = NEW_TARGET;
          }
        } else if (renderInfo === null) {
          pathTransitionCase = FIRST_TARGET_NOT_RENDERED;
        } else if (renderInfo.path !== path) {
          if (renderInfo.transitionState === "exited") {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED;
          } else {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING;
          }
        } else {
          pathTransitionCase = TARGET_RENDERED;
        }
      } else {
        if (renderInfo !== null && renderInfo.path === path) {
          if (
            renderInfo.transitionState !== "exiting" &&
            renderInfo.transitionState !== "exited"
          ) {
            pathTransitionCase = NOT_TARGET_AND_NEED_TO_START_EXITING;
          } else {
            pathTransitionCase = NOT_TARGET_AND_EXITING;
          }
        } else {
          pathTransitionCase = NOT_TARGET;
        }
      }
      useEffect(() => {
        switch (pathTransitionCase) {
          case NEW_TARGET_MATCHES_EXITING_PATH:
            exitTimelineAndDone.timeline.kill();
            exitTimelineAndDone.done();
            setExitTimelineAndDone({});
            // Making it look like we exited some other path, in
            // order to restart the transition into this path.
            setRenderInfo({
              path: path + "-exited",
              transitionState: "exited"
            });
            setTargetPath(path);
            break;
          case NEW_TARGET:
            setTargetPath(path);
            break;
          case FIRST_TARGET_NOT_RENDERED:
            setRenderInfo({ path: path });
            break;
          case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
            setRenderInfo({ path: path, transitionState: "entering" });
            break;
          case NOT_TARGET_AND_NEED_TO_START_EXITING:
            setRenderInfo({ ...renderInfo, transitionState: "exiting" });
            break;
          // case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          // case NOT_TARGET:
          default:
          // no-op
        }
      });
      return {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      };
    };
    
    const TransitionManager = props => {
      const {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      } = usePathTransitionCase(props.path, props.match);
      const getEnterTransition = show => (
        <Transition
          key={props.path}
          addEndListener={slowFadeInAndDropFromAboveThenLeftRight()}
          in={show}
          unmountOnExit={true}
        >
          {state => {
            const Child = props.component;
            console.log(props.path + ": " + state);
            return <Child state={state} />;
          }}
        </Transition>
      );
      const getExitTransition = () => {
        return (
          <Transition
            key={props.path}
            addEndListener={slowFadeOutAndDrop(setExitTimelineAndDone)}
            in={false}
            onExited={() =>
              setRenderInfo({ ...renderInfo, transitionState: "exited" })
            }
            unmountOnExit={true}
          >
            {state => {
              const Child = props.component;
              console.log(props.path + ": " + state);
              return <Child state={state} />;
            }}
          </Transition>
        );
      };
      switch (pathTransitionCase) {
        case NEW_TARGET_MATCHES_EXITING_PATH:
        case NEW_TARGET:
        case FIRST_TARGET_NOT_RENDERED:
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          return null;
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
          return getEnterTransition(false);
        case TARGET_RENDERED:
          return getEnterTransition(true);
        case NOT_TARGET_AND_NEED_TO_START_EXITING:
        case NOT_TARGET_AND_EXITING:
          return getExitTransition();
        // case NOT_TARGET:
        default:
          return null;
      }
    };
    export default TransitionManager;
    
    import { TimelineMax } from "gsap";
    const startStyle = { autoAlpha: 0, y: -50 };
    export const slowFadeInAndDropFromAboveThenLeftRight = trackTimelineAndDone => (
      node,
      done
    ) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.set(node, startStyle);
      timeline
        .to(node, 0.5, {
          autoAlpha: 1,
          y: 0
        })
        .to(node, 0.5, { x: -25 })
        .to(node, 0.5, {
          x: 0,
          onComplete: done
        });
    };
    export const slowFadeOutAndDrop = trackTimelineAndDone => (node, done) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.to(node, 2, {
        autoAlpha: 0,
        y: 100,
        onComplete: done
      });
    };
    
    TransitionRoute.js

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    
    import React from "react";
    import { BrowserRouter } from "react-router-dom";
    import LinkOrStatic from "./LinkOrStatic";
    import { componentInfoArray } from "./components";
    import {
      useTransitionContextState,
      TransitionContext
    } from "./TransitionContext";
    import TransitionRoute from "./TransitionRoute";
    
    const App = props => {
      const transitionContext = useTransitionContextState();
      return (
        <TransitionContext.Provider value={transitionContext}>
          <BrowserRouter>
            <div>
              <br />
              {componentInfoArray.map(compInfo => (
                <LinkOrStatic key={compInfo.path} to={compInfo.path}>
                  {compInfo.linkText}
                </LinkOrStatic>
              ))}
    
              {componentInfoArray.map(compInfo => (
                <TransitionRoute
                  key={compInfo.path}
                  path={compInfo.path}
                  exact
                  component={compInfo.component}
                />
              ))}
            </div>
          </BrowserRouter>
        </TransitionContext.Provider>
      );
    };
    export default App;
    
    import React, { useState } from "react";
    
    export const TransitionContext = React.createContext();
    export const useTransitionContextState = () => {
      // The path most recently requested by the user
      const [targetPath, setTargetPath] = useState(null);
      // The path currently rendered. If different than the target path,
      // then probably in the middle of a transition.
      const [renderInfo, setRenderInfo] = useState(null);
      const [exitTimelineAndDone, setExitTimelineAndDone] = useState({});
      const transitionContext = {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      };
      return transitionContext;
    };
    
    import React from "react";
    const Home = props => {
      return <div>Hello {props.state + " Home!"}</div>;
    };
    const ProjectOne = props => {
      return <div>Hello {props.state + " Project One!"}</div>;
    };
    const ProjectTwo = props => {
      return <div>Hello {props.state + " Project Two!"}</div>;
    };
    export const componentInfoArray = [
      {
        linkText: "Home",
        component: Home,
        path: "/"
      },
      {
        linkText: "Show project one",
        component: ProjectOne,
        path: "/projects/one"
      },
      {
        linkText: "Show project two",
        component: ProjectTwo,
        path: "/projects/two"
      }
    ];
    
    import React from "react";
    import { Route, Link } from "react-router-dom";
    
    const LinkOrStatic = props => {
      const path = props.to;
      return (
        <>
          <Route exact path={path}>
            {({ match }) => {
              if (match) {
                return props.children;
              }
              return (
                <Link className={props.className} to={props.to}>
                  {props.children}
                </Link>
              );
            }}
          </Route>
          <br />
        </>
      );
    };
    export default LinkOrStatic;
    
    import React from "react";
    import { Route } from "react-router-dom";
    import TransitionManager from "./TransitionManager";
    
    const TransitionRoute = props => {
      return (
        <Route path={props.path} exact>
          {({ match }) => {
            return (
              <TransitionManager
                key={props.path}
                path={props.path}
                component={props.component}
                match={match}
              />
            );
          }}
        </Route>
      );
    };
    export default TransitionRoute;
    
    import React, { useContext, useEffect } from "react";
    import { Transition } from "react-transition-group";
    import {
      slowFadeInAndDropFromAboveThenLeftRight,
      slowFadeOutAndDrop
    } from "./animations";
    import { TransitionContext } from "./TransitionContext";
    
    const NEW_TARGET = "NEW_TARGET";
    const NEW_TARGET_MATCHES_EXITING_PATH = "NEW_TARGET_MATCHES_EXITING_PATH";
    const FIRST_TARGET_NOT_RENDERED = "FIRST_TARGET_NOT_RENDERED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING";
    const TARGET_RENDERED = "TARGET_RENDERED";
    const NOT_TARGET_AND_NEED_TO_START_EXITING =
      "NOT_TARGET_AND_NEED_TO_START_EXITING";
    const NOT_TARGET_AND_EXITING = "NOT_TARGET_AND_EXITING";
    const NOT_TARGET = "NOT_TARGET";
    const usePathTransitionCase = (path, match) => {
      const {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      } = useContext(TransitionContext);
      let pathTransitionCase = null;
      if (match) {
        if (targetPath !== path) {
          if (
            renderInfo &&
            renderInfo.path === path &&
            renderInfo.transitionState === "exiting" &&
            exitTimelineAndDone.timeline
          ) {
            pathTransitionCase = NEW_TARGET_MATCHES_EXITING_PATH;
          } else {
            pathTransitionCase = NEW_TARGET;
          }
        } else if (renderInfo === null) {
          pathTransitionCase = FIRST_TARGET_NOT_RENDERED;
        } else if (renderInfo.path !== path) {
          if (renderInfo.transitionState === "exited") {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED;
          } else {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING;
          }
        } else {
          pathTransitionCase = TARGET_RENDERED;
        }
      } else {
        if (renderInfo !== null && renderInfo.path === path) {
          if (
            renderInfo.transitionState !== "exiting" &&
            renderInfo.transitionState !== "exited"
          ) {
            pathTransitionCase = NOT_TARGET_AND_NEED_TO_START_EXITING;
          } else {
            pathTransitionCase = NOT_TARGET_AND_EXITING;
          }
        } else {
          pathTransitionCase = NOT_TARGET;
        }
      }
      useEffect(() => {
        switch (pathTransitionCase) {
          case NEW_TARGET_MATCHES_EXITING_PATH:
            exitTimelineAndDone.timeline.kill();
            exitTimelineAndDone.done();
            setExitTimelineAndDone({});
            // Making it look like we exited some other path, in
            // order to restart the transition into this path.
            setRenderInfo({
              path: path + "-exited",
              transitionState: "exited"
            });
            setTargetPath(path);
            break;
          case NEW_TARGET:
            setTargetPath(path);
            break;
          case FIRST_TARGET_NOT_RENDERED:
            setRenderInfo({ path: path });
            break;
          case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
            setRenderInfo({ path: path, transitionState: "entering" });
            break;
          case NOT_TARGET_AND_NEED_TO_START_EXITING:
            setRenderInfo({ ...renderInfo, transitionState: "exiting" });
            break;
          // case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          // case NOT_TARGET:
          default:
          // no-op
        }
      });
      return {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      };
    };
    
    const TransitionManager = props => {
      const {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      } = usePathTransitionCase(props.path, props.match);
      const getEnterTransition = show => (
        <Transition
          key={props.path}
          addEndListener={slowFadeInAndDropFromAboveThenLeftRight()}
          in={show}
          unmountOnExit={true}
        >
          {state => {
            const Child = props.component;
            console.log(props.path + ": " + state);
            return <Child state={state} />;
          }}
        </Transition>
      );
      const getExitTransition = () => {
        return (
          <Transition
            key={props.path}
            addEndListener={slowFadeOutAndDrop(setExitTimelineAndDone)}
            in={false}
            onExited={() =>
              setRenderInfo({ ...renderInfo, transitionState: "exited" })
            }
            unmountOnExit={true}
          >
            {state => {
              const Child = props.component;
              console.log(props.path + ": " + state);
              return <Child state={state} />;
            }}
          </Transition>
        );
      };
      switch (pathTransitionCase) {
        case NEW_TARGET_MATCHES_EXITING_PATH:
        case NEW_TARGET:
        case FIRST_TARGET_NOT_RENDERED:
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          return null;
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
          return getEnterTransition(false);
        case TARGET_RENDERED:
          return getEnterTransition(true);
        case NOT_TARGET_AND_NEED_TO_START_EXITING:
        case NOT_TARGET_AND_EXITING:
          return getExitTransition();
        // case NOT_TARGET:
        default:
          return null;
      }
    };
    export default TransitionManager;
    
    import { TimelineMax } from "gsap";
    const startStyle = { autoAlpha: 0, y: -50 };
    export const slowFadeInAndDropFromAboveThenLeftRight = trackTimelineAndDone => (
      node,
      done
    ) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.set(node, startStyle);
      timeline
        .to(node, 0.5, {
          autoAlpha: 1,
          y: 0
        })
        .to(node, 0.5, { x: -25 })
        .to(node, 0.5, {
          x: 0,
          onComplete: done
        });
    };
    export const slowFadeOutAndDrop = trackTimelineAndDone => (node, done) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.to(node, 2, {
        autoAlpha: 0,
        y: 100,
        onComplete: done
      });
    };
    
    animations.js

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    
    import React from "react";
    import { BrowserRouter } from "react-router-dom";
    import LinkOrStatic from "./LinkOrStatic";
    import { componentInfoArray } from "./components";
    import {
      useTransitionContextState,
      TransitionContext
    } from "./TransitionContext";
    import TransitionRoute from "./TransitionRoute";
    
    const App = props => {
      const transitionContext = useTransitionContextState();
      return (
        <TransitionContext.Provider value={transitionContext}>
          <BrowserRouter>
            <div>
              <br />
              {componentInfoArray.map(compInfo => (
                <LinkOrStatic key={compInfo.path} to={compInfo.path}>
                  {compInfo.linkText}
                </LinkOrStatic>
              ))}
    
              {componentInfoArray.map(compInfo => (
                <TransitionRoute
                  key={compInfo.path}
                  path={compInfo.path}
                  exact
                  component={compInfo.component}
                />
              ))}
            </div>
          </BrowserRouter>
        </TransitionContext.Provider>
      );
    };
    export default App;
    
    import React, { useState } from "react";
    
    export const TransitionContext = React.createContext();
    export const useTransitionContextState = () => {
      // The path most recently requested by the user
      const [targetPath, setTargetPath] = useState(null);
      // The path currently rendered. If different than the target path,
      // then probably in the middle of a transition.
      const [renderInfo, setRenderInfo] = useState(null);
      const [exitTimelineAndDone, setExitTimelineAndDone] = useState({});
      const transitionContext = {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      };
      return transitionContext;
    };
    
    import React from "react";
    const Home = props => {
      return <div>Hello {props.state + " Home!"}</div>;
    };
    const ProjectOne = props => {
      return <div>Hello {props.state + " Project One!"}</div>;
    };
    const ProjectTwo = props => {
      return <div>Hello {props.state + " Project Two!"}</div>;
    };
    export const componentInfoArray = [
      {
        linkText: "Home",
        component: Home,
        path: "/"
      },
      {
        linkText: "Show project one",
        component: ProjectOne,
        path: "/projects/one"
      },
      {
        linkText: "Show project two",
        component: ProjectTwo,
        path: "/projects/two"
      }
    ];
    
    import React from "react";
    import { Route, Link } from "react-router-dom";
    
    const LinkOrStatic = props => {
      const path = props.to;
      return (
        <>
          <Route exact path={path}>
            {({ match }) => {
              if (match) {
                return props.children;
              }
              return (
                <Link className={props.className} to={props.to}>
                  {props.children}
                </Link>
              );
            }}
          </Route>
          <br />
        </>
      );
    };
    export default LinkOrStatic;
    
    import React from "react";
    import { Route } from "react-router-dom";
    import TransitionManager from "./TransitionManager";
    
    const TransitionRoute = props => {
      return (
        <Route path={props.path} exact>
          {({ match }) => {
            return (
              <TransitionManager
                key={props.path}
                path={props.path}
                component={props.component}
                match={match}
              />
            );
          }}
        </Route>
      );
    };
    export default TransitionRoute;
    
    import React, { useContext, useEffect } from "react";
    import { Transition } from "react-transition-group";
    import {
      slowFadeInAndDropFromAboveThenLeftRight,
      slowFadeOutAndDrop
    } from "./animations";
    import { TransitionContext } from "./TransitionContext";
    
    const NEW_TARGET = "NEW_TARGET";
    const NEW_TARGET_MATCHES_EXITING_PATH = "NEW_TARGET_MATCHES_EXITING_PATH";
    const FIRST_TARGET_NOT_RENDERED = "FIRST_TARGET_NOT_RENDERED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED";
    const TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING =
      "TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING";
    const TARGET_RENDERED = "TARGET_RENDERED";
    const NOT_TARGET_AND_NEED_TO_START_EXITING =
      "NOT_TARGET_AND_NEED_TO_START_EXITING";
    const NOT_TARGET_AND_EXITING = "NOT_TARGET_AND_EXITING";
    const NOT_TARGET = "NOT_TARGET";
    const usePathTransitionCase = (path, match) => {
      const {
        targetPath,
        setTargetPath,
        renderInfo,
        setRenderInfo,
        exitTimelineAndDone,
        setExitTimelineAndDone
      } = useContext(TransitionContext);
      let pathTransitionCase = null;
      if (match) {
        if (targetPath !== path) {
          if (
            renderInfo &&
            renderInfo.path === path &&
            renderInfo.transitionState === "exiting" &&
            exitTimelineAndDone.timeline
          ) {
            pathTransitionCase = NEW_TARGET_MATCHES_EXITING_PATH;
          } else {
            pathTransitionCase = NEW_TARGET;
          }
        } else if (renderInfo === null) {
          pathTransitionCase = FIRST_TARGET_NOT_RENDERED;
        } else if (renderInfo.path !== path) {
          if (renderInfo.transitionState === "exited") {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED;
          } else {
            pathTransitionCase = TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING;
          }
        } else {
          pathTransitionCase = TARGET_RENDERED;
        }
      } else {
        if (renderInfo !== null && renderInfo.path === path) {
          if (
            renderInfo.transitionState !== "exiting" &&
            renderInfo.transitionState !== "exited"
          ) {
            pathTransitionCase = NOT_TARGET_AND_NEED_TO_START_EXITING;
          } else {
            pathTransitionCase = NOT_TARGET_AND_EXITING;
          }
        } else {
          pathTransitionCase = NOT_TARGET;
        }
      }
      useEffect(() => {
        switch (pathTransitionCase) {
          case NEW_TARGET_MATCHES_EXITING_PATH:
            exitTimelineAndDone.timeline.kill();
            exitTimelineAndDone.done();
            setExitTimelineAndDone({});
            // Making it look like we exited some other path, in
            // order to restart the transition into this path.
            setRenderInfo({
              path: path + "-exited",
              transitionState: "exited"
            });
            setTargetPath(path);
            break;
          case NEW_TARGET:
            setTargetPath(path);
            break;
          case FIRST_TARGET_NOT_RENDERED:
            setRenderInfo({ path: path });
            break;
          case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
            setRenderInfo({ path: path, transitionState: "entering" });
            break;
          case NOT_TARGET_AND_NEED_TO_START_EXITING:
            setRenderInfo({ ...renderInfo, transitionState: "exiting" });
            break;
          // case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          // case NOT_TARGET:
          default:
          // no-op
        }
      });
      return {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      };
    };
    
    const TransitionManager = props => {
      const {
        renderInfo,
        setRenderInfo,
        setExitTimelineAndDone,
        pathTransitionCase
      } = usePathTransitionCase(props.path, props.match);
      const getEnterTransition = show => (
        <Transition
          key={props.path}
          addEndListener={slowFadeInAndDropFromAboveThenLeftRight()}
          in={show}
          unmountOnExit={true}
        >
          {state => {
            const Child = props.component;
            console.log(props.path + ": " + state);
            return <Child state={state} />;
          }}
        </Transition>
      );
      const getExitTransition = () => {
        return (
          <Transition
            key={props.path}
            addEndListener={slowFadeOutAndDrop(setExitTimelineAndDone)}
            in={false}
            onExited={() =>
              setRenderInfo({ ...renderInfo, transitionState: "exited" })
            }
            unmountOnExit={true}
          >
            {state => {
              const Child = props.component;
              console.log(props.path + ": " + state);
              return <Child state={state} />;
            }}
          </Transition>
        );
      };
      switch (pathTransitionCase) {
        case NEW_TARGET_MATCHES_EXITING_PATH:
        case NEW_TARGET:
        case FIRST_TARGET_NOT_RENDERED:
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITING:
          return null;
        case TARGET_NOT_RENDERED_AND_RENDER_PATH_EXITED:
          return getEnterTransition(false);
        case TARGET_RENDERED:
          return getEnterTransition(true);
        case NOT_TARGET_AND_NEED_TO_START_EXITING:
        case NOT_TARGET_AND_EXITING:
          return getExitTransition();
        // case NOT_TARGET:
        default:
          return null;
      }
    };
    export default TransitionManager;
    
    import { TimelineMax } from "gsap";
    const startStyle = { autoAlpha: 0, y: -50 };
    export const slowFadeInAndDropFromAboveThenLeftRight = trackTimelineAndDone => (
      node,
      done
    ) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.set(node, startStyle);
      timeline
        .to(node, 0.5, {
          autoAlpha: 1,
          y: 0
        })
        .to(node, 0.5, { x: -25 })
        .to(node, 0.5, {
          x: 0,
          onComplete: done
        });
    };
    export const slowFadeOutAndDrop = trackTimelineAndDone => (node, done) => {
      const timeline = new TimelineMax();
      if (trackTimelineAndDone) {
        trackTimelineAndDone({ timeline, done });
      }
      timeline.to(node, 2, {
        autoAlpha: 0,
        y: 100,
        onComplete: done
      });
    };
    

    非常有用的答案,谢谢!然而,我在最后寻找的是,如果用户在“/”(Home),单击“/projects/one”(projects),我希望Home在安装和转换下一个路由(projects)之前首先动画输出。延迟但是如果我有3条或更多的路线,每一条都有不同的延迟,但我总是想确保我们离开的路线将完全动画化,然后看看“好的,什么是路线?”