let me give it a shot. At least will point to a direction
So basically problem comes down to 2 things:
- Keeping both route pages in VirtualDOM for the time of transition.
- Having 2 DOM elements in the same place at once.
Short answer to your question would be no, it's not possible using the tools you want to use. As to your guess why it doesn't work you guessed 1 reason correctly. Old route page is immediately removed from DOM upon route change, the very same frame new route page is rendered. Another problem would be pages sharing the same spot in DOM, so one of them would have to be position: absolute;
to be on top of other page content.
Thing you are trying to do is overall not easy nor walked path. If you look at apps you will see nice transitions and animations showing both previous and next state of the app. For web tho, it's never the case, yes you have transitions and nice animations, but never between pages.
So okey, i can't have it my way and you are telling me even with custom solution it would be hard. So what now, what are my options?
I will give you 3 separate ways you can achieve the wanted effect, starting from least effort.
Option 1 - Hack it.
You can hack the solution. Have a onclick listener that instead of changing the route triggers animation transition out for the current page. The same event can start a timeout event, that 500ms later would change the route, which would trigger the new page to appear with transition in.
Option 2 - Take responsibility from Router
Instead of using routing (/routeName
) use route parameter (/:routeName
), thus taking away rendering responsability from router and handeling it manually. What you can have is something similar to carousel or custom animation with 2 pages rendered at once. :routeName
will tell you which page to currently show, and you can still show/render previous page for purposes of transition.
<Route path="/:routeName"><MyComponent /></Route>
Component
export function MyComponent() {
const { params } = useRoute();
const prevPageRoute = useRef();
const prevPageContainer = useRef();
const currentPageContainer = useRef();
const routeNameToComponent = {
'Subtest1': Subtest1
'Subtest2': Subtest2
}
const PreviousPage = routeNameToComponent[prevPageRoute.current];
const CurrentPage = routeNameToComponent[params.routeName];
useLayoutEffect(() => {
return () => prevRouteName.current = params.routeName;
}, [params.routeName])
useLayoutEffect(() => {
if(!previousPageContainer.current) return;
// Both pages are in DOM, lets animate them manually.
currentPageContainer.current.classList.add('position-right');
currentPageContainer.current.getBoundingClientRect(); // Force layout recalc
currentPageContainer.current.classList.remove('position-right');
previousPageContainer.current.classList.add('position-left');
// here you can add timeout to remove prevPage from DOM, dont forget to clearTimeout to avoid weirdness on fast clicks with return () => clearTimeout(id).
}, [params.routeName])
return
<div style={{position: 'relative'}}>
{prevPageRoute.current && <div ref={previousPageContainer} style={{position: 'absolute', right: 0, ...}}><PreviousPage /></div>}
<div ref={currentPageContainer}><CurrentPage /></div>
</div>
// alternative using Transition, remove the second useLayoutEffect, in this case no need to animate manually
return
<div style={{position: 'relative'}}>
{prevPageRoute.current && <Transition key={'prev' + prevPageRoute.current} style={{position: 'absolute', right: 0, ...}}><PreviousPage /></Transition>}
<Transition key={'current' + params.routeName}><CurrentPage /></Transition>
</div>
}
Option 3 - Custom solution
Make your own RouteTransition component. Basic principle is very easy. They work the same way Transition component works, you have props and you pass children that need animation. Difference is that on unmount of this component, instead of unmounting you will keep it in DOM for length of transition.
We need to know who is the parent element, so that we know where to render our transition out page, also it needs to be positioned based on parent location.
Also we need to upon unmount animate the page component leaving from parent element. For that we need to keep it mounted. To do it we will cheat react by adding our transition out page into separate VirtualDOM branch, thus it will have independent rendering and life cycle.
First lets create independent VirtualDOM branch where transitioned out element will live. It can be included at the very top of the App tree or even as separate React.render() (just needs to be rendered all the time). It will render our tansitions using react portal to have them in correct places.
let ID = 1;
export const transitionState = {
items: [],
rerender: undefined,
add: (item) => {state.items.push({...item, id: ID++}); rerender?.()}
remove: (id) => {state.items.filter((item) => item.id !== id); rerender?.()}
}
export function TransitionRenderer() {
const [_, setNewState] = useState(1);
const rerender = () => setNewState((value) => value+1)
transitionState.rerender = rerender;
return <>{transitionState.items.map(item => <TransitionItem item={item} />)}</>
}
export function TransitionItem(item) {
useLayoutEffect(() => {
setTimeout(() => transitionState.remove(item.id), 500)
}, [])
//parent needs to be position:relative
return createPortal(<div className="absolute right-0 etc."><Transition {...item.transitionProps}>{item.children}</Transition></div>, item.parent)
}
Now we need the RouteTransition component, that will wrap our routes and act as regular Transition component, only difference being on unmount it will gain a second life in our TransitionRenderer.
import transitionState;
export function RouteTransition(props) {
const ref = useRef();
//this will trigger on unmount
useLayoutEffect(() => () => transitionState.add({children: props.children, parent: ref.current.parentElement, transitionProps: props.animationOut}), [])
return <div ref={ref}>
<Transition {...props.animationIn}>
{props.children}
</Transition>
</div>
}
And finally this would be our Route, very minimal changes.
<Route path="/Subtest1">
<RouteTransition
animationIn={...}
animationOut={...}
>
<Subtest1 />
</RouteTransition>
</Route>
All of the code provided is pseudo-code written directly in stack-overflow little text box. There might be something I missed, but logic behind all the code should be sound and working. If you get stuck don't hesitate to comment, will try to respond. Understanding how it works and what's happening requires average/decent understanding of react.
Hope I was of help.