In ReactJS is it possible to use HeadlessUI to transition between components that are loaded through Route? If so how?
Asked Answered
C

1

6

I'm hoping for a bit of help here if possible. I'm developing a project in React and using TailwindCSS and HeadlessUI for transitions. Is it possible to use HeadlessUI transitions with route? I cannot get a component to transition out when loading the next component with route. I basically have a page appear from the right and fill the screen. Then when a button is clicked the page should disappear off to the left and the next page should load in from the right at the same time, the next page slightly overlapping the previous page as they both transition. I can do this quite easily using state changes etc but when it comes to wrapping the components in a route with a path I cannot for the life of me solve the puzzle. The enter transition works but the leave transition just will not work and the component vanishes. Presumably the component is unloaded before the leave transition gets to play. So how do I prevent the component from being unloaded until after the leave transition has played out?

<Route path="/Subtest1">
  <Transition
    appear={true}
    show={true}
    enter="transform duration-500"
    enterFrom={"translate-x-full"}
    enterTo="translate-x-0"
    leave="transform duration-500"
    leaveFrom="translate-x-0"
    leaveTo={"filter brightness-50 -translate-x-1/2"}
    as="div"
    className={"absolute z-20 top-0 left-0 right-0 w-full h-full flex items-center justify-center"}
  >
    <Subtest1 />
  </Transition>
</Route>
<Route path="/Subtest2">
  <Transition
    appear={true}
    show={true}
    enter="transform duration-500"
    enterFrom={"translate-x-full"}
    enterTo="translate-x-0"
    leave="transform duration-500"
    leaveFrom="translate-x-0"
    leaveTo={"filter brightness-50 -translate-x-1/2"}
    as="div"
    className={"absolute z-20 top-0 left-0 right-0 w-full h-full flex items-center justify-center"}
  >
    <Subtest2 />
  </Transition>
</Route>

I would really appreciate any help on this as it's been a very long week trying to solve these transitions. Can it actually be done?

Thanks in advance.

Chromophore answered 26/9, 2021 at 21:21 Comment(0)
W
0

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.

Westney answered 30/6 at 15:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.