反应路由器转换进出事件 [英] React Router transition in and out events

查看:37
本文介绍了反应路由器转换进出事件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我对我正在处理的一个小型网站进行了相当基本的设置.我正在使用 React 和 React Router 4.现在我想在用户输入路由时添加转换,以使用一些 javascript 动画转换该路由的 IN 和 OUT.但是,我不知道如何正确执行此操作?假设用户在/并单击导航到/projects/one 的链接,那么我如何为此启动转换 IN,如果用户导航离开以启动该组件/路由的转换 OUT?我不希望东西只是卸载",我希望它们在过渡之间平滑并有控制权......?超时值只是一个示例时间.

目前我有以下几点:

更新:

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

代码:

I have a fairly basic setup for a small website I'm working on. I'm using React and React Router 4. Now I would like to add transition to when the user is entering a route, to transition IN and OUT that route with some javascript animation. However, I can't figure out how to do this properly? Let say the user is at / and clicks a link that navigates to /projects/one, then how can I then start the transition IN for that, and if the user navigates away to start the transition OUT for that component/route? I don't want stuff to just "unmount", I want them to be smooth between the transitions and to have control..? The timeout value is just an example time.

At the moment I have the following:

UPDATE:

Based on Ryan C code sample I've been able to come up with a solution which is getting really close to what I would like to have, and thereby removed my old code since it was way too much away from my initial question.

Code: https://codesandbox.io/s/k2r02r378o

For this current version I have currently two questions that I cant figure out...

  1. If the user is currently at HomePage (/) and a user clicks on a Link for that same path, how can I prevent my transition flow from happen, and just kind of do nothing? And at the same time not adding a lots of history with same path in the browser?

  2. If the user is at HomePage (/) and navigates to ProjectsPage (/projects/one), and before the transition finishes the user navigates back to HomePage (/) again, then I would like the "transitionOut" of HomePage to stop where it is, and run "transitionIn" again (kind of rewind my transition out tween).. Maybe it's connected to 1)?

解决方案

So it turns out that an approach that supports restarting the entering transition if you switch from route 1 to route 2 and then back to route 1 while route 1 is still exiting is pretty tricky. There may be some minor issues in what I have, but I think the overall approach is sound.

The overall approach involves separating out a target path (where the user wants to go) from the rendering path (that path currently displayed which may be in a transitioning state). In order to make sure a transition occurs at the appropriate times, state is used to sequence things step by step (e.g. first render a transition with in=false, then render with in=true for an entering transition). The bulk of the complexity is handled within TransitionManager.js.

I've used hooks in my code because it was easier for me to work through the logic without the syntax overhead of classes, so for the next couple months or so, this will only work with the alpha. If the hooks implementation changes in the official release in any way that breaks this code, I will update this answer at that time.

Here's the code:

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

这篇关于反应路由器转换进出事件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆