Javascript 反应路由器转换输入和输出事件
我有一个相当基本的设置为一个小网站,我的工作。我正在使用React和React路由器4。现在,我想在用户进入一条路线时添加一个过渡,通过一些javascript动画在该路线中来回过渡。然而,我不知道如何正确地做到这一点?假设用户位于/并单击导航到/projects/one的链接,那么我如何才能为此启动转换,如果用户导航到该组件/路由,则如何启动转换?我不希望东西只是“卸载”,我希望它们在转换之间保持平稳,并拥有控制权。。? 超时值只是一个示例时间 目前,我有以下几点: 更新: 基于Ryan C代码示例,我已经能够提出一个非常接近我想要的解决方案,因此删除了我的旧代码,因为它离我最初的问题太远了 代码: 对于目前的版本,我有两个问题无法解决Javascript 反应路由器转换输入和输出事件,javascript,reactjs,react-router,transition,Javascript,Reactjs,React Router,Transition,我有一个相当基本的设置为一个小网站,我的工作。我正在使用React和React路由器4。现在,我想在用户进入一条路线时添加一个过渡,通过一些javascript动画在该路线中来回过渡。然而,我不知道如何正确地做到这一点?假设用户位于/并单击导航到/projects/one的链接,那么我如何才能为此启动转换,如果用户导航到该组件/路由,则如何启动转换?我不希望东西只是“卸载”,我希望它们在转换之间保持平稳,并拥有控制权。。? 超时值只是一个示例时间 目前,我有以下几点: 更新: 基于Ryan C代
我保留了这个答案,以便评论仍然有意义,并且可以看到变化,但这已经被 以下是一些相关参考资料,我希望您已经了解其中一些: 使用下面包含的代码,您可以快速看到效果 下面的代码使用
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条或更多的路线,每一条都有不同的延迟,但我总是想确保我们离开的路线将完全动画化,然后看看“好的,什么是路线?”