Таким образом, оказывается, что подход, который поддерживает перезапуск входящего перехода, если вы переключаетесь с маршрута 1 на маршрут 2, а затем обратно на маршрут 1, когда маршрут 1 все еще выходит, довольно сложен.Могут быть некоторые незначительные проблемы в том, что у меня есть, но я думаю, что общий подход оправдан.
Общий подход включает в себя отделение целевого пути (куда пользователь хочет идти) от пути рендеринга (этот путьв настоящее время отображается, который может быть в переходном состоянии).Чтобы убедиться, что переход происходит в подходящее время, используется состояние для последовательной последовательности действий (например, сначала визуализируйте переход с помощью in=false
, а затем визуализируйте с in=true
для входящего перехода).Большая часть сложности обрабатывается в TransitionManager.js
.
. Я использовал хуки в своем коде, потому что мне было легче работать с логикой без дополнительных затрат на синтаксис классов, так что в течение следующих нескольких месяцевили так, это будет работать только с альфа.Если реализация хуков изменится в официальном выпуске каким-либо образом, нарушающим этот код, я обновлю этот ответ в то время.
Вот код:
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
});
};