React Router跳入和跳出事件

时间:2018-11-23 20:21:48

标签: javascript reactjs react-router transition

我正在为一个正在工作的小型网站进行基本的设置。我正在使用React和React Router4。现在,我想在用户输入路线时向其中添加过渡,以使用一些JavaScript动画在该路线中进行输入和输出过渡。但是,我不知道如何正确执行此操作?假设用户位于/并单击导航到/ projects / one的链接,那么我如何才能为此启动转换IN,如果用户离开以启动该组件/路由的转换OUT?我不想让东西“卸载”,我希望它们在过渡之间保持顺畅并拥有控制权。 超时值只是一个示例时间。

目前,我有以下内容:

更新:

基于Ryan C代码示例,我已经能够提出一种非常接近我想要的解决方案的方法,从而删除了我的旧代码,因为它与我最初的问题相去甚远。

代码:https://codesandbox.io/s/k2r02r378o

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

  1. 如果用户当前位于主页(/)上,并且用户单击该链接的同一路径,那么如何防止发生过渡流,而又什么也不做?同时没有在浏览器中添加很多具有相同路径的历史记录吗?

  2. 如果用户位于Homepage(/)并导航到ProjectsPage(/ projects / one),并且在转换完成之前,用户再次导航回HomePage(/),那么我想使用“ transitionOut”的HomePage停止它的位置,然后再次运行“ transitionIn”(有点快将我的过渡倒退到补间)。也许它已连接到1)?

2 个答案:

答案 0 :(得分:0)

我将这个答案留在原处,以便使注释仍然有意义,并且可以看到演变过程,但是my new answer

已取代

以下是一些相关的参考资料,我希望您已经看过其中的一些内容:

https://reacttraining.com/react-router/web/api/Route/children-func

https://reactcommunity.org/react-transition-group/transition

https://greensock.com/react

code sandbox(包含下面的代码),因此您可以快速看到效果

下面的代码使用addEndListener属性在内部内的过渡使用gsap插入自定义动画。使这项工作有两个重要方面。为了使Transition处于entering状态,in属性必须从falsetrue。如果它从true开始,那么它将立即跳到entered状态,而不会过渡。为了使这种情况在Route中发生,您需要使用Route的children属性(而不是componentrender),因为这样子节点将被渲染无论路线是否匹配。在下面的示例中,您将看到:

<Route exact path="/projects/one">
    {({ match }) => <Projects show={match !== null} />}
</Route>

这会将布尔show属性传递给组件,该属性仅在Route匹配时才为true。然后,它将作为in的{​​{1}}属性传递。这允许TransitionProjects开头,而不是(当使用in={false}属性时)根本就没有呈现(这将防止发生过渡,因为过渡将具有{ {1}}(首次呈现时)。

我没有完全消化您在Route component项目中试图做的所有事情(我的示例已大大简化,但确实做了多步gsap动画),但我认为您会更好可以使用in={true}来控制所有动画的触发,而不必同时使用componentDidMountTransition

这是第一个代码版本:

Transition

更新1:解决了更新中的问题1。 react-router版本4的一件好事是,路由可以出现在多个位置并控制页面的多个部分。在此code sandbox中,我完成了代码沙箱的更新,以使Home链接在链接和静态文本之间切换(尽管您可以将其更改为使用样式,从而使两者的外观相同)。我用LinkOrStaticText替换了Link(我很快完成了这一步,它可以使用一些改进来更可靠地处理传递的道具):

componentDidMount

我将单独进行更新以解决问题2。

更新2 :在尝试解决问题2时,我发现了我在此答案中使用的方法的一些基本问题。由于在某些情况下一次执行多条路由以及正在进行的未安装转换的奇数残留问题,该行为变得混乱。我需要使用不同的方法从头开始,所以我将修订的方法发布在单独的答案中。

答案 1 :(得分:0)

因此,事实证明,如果从路线1切换到路线2,然后在路线1仍退出时又回到路线1,则支持重启进入的过渡的方法非常棘手。我所拥有的可能有一些小问题,但我认为总体方法是合理的。

总体方法涉及从渲染路径(当前显示的路径可能处于过渡状态)中分离出目标路径(用户要去的地方)。为了确保在适当的时间发生转场,请使用state逐步对事物进行排序(例如,首先使用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);

App.js

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;

TransitionContext.js

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

components.js

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"
  }
];

LinkOrStatic.js

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;

TransitionRoute.js

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;

TransitionManager.js

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;

animations.js

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

Edit nrnz2lvnxj