因此,事实证明,如果您从路线 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);
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
});
};