framer / motion

Open source, production-ready animation and gesture library for React
https://framer.com/motion
MIT License
22.41k stars 740 forks source link

Unmount animations for Suspense Fallback component? #1193

Closed samselikoff closed 5 months ago

samselikoff commented 2 years ago

It looks like Suspense currently doesn't support unmount animations for its Fallback component. There are a few related discussions/RFCs in the React repo like this one.

Curious if there are currently any known/good workarounds for this? I've been toying with something like this example from a SO answer where I force a data-loading component to continue suspending until after a completes a fadeOut animation using a MotionValue and animate() function, but haven't landed on anything decent yet.

Just wondering if anyone has any good alternatives for the time being!

lunelson commented 2 years ago

I'd also be really interested in any solutions to this. I'm trying to figure out how to do page transitions, but wait for the sections of the page which are code-split.

lunelson commented 2 years ago

@samselikoff @mattgperry on a related note:

Could it be possible to coordinate Suspense with AnimatePresence in such a way as to avoid rendering the fallback—or in other words, wait for the suspended component to resolve? I'm thinking of a page-transition scenario where AnimatePresence is wrapped around a Page component that contains async items (think loadable-components, or next/dynamic).

Naïve example: here I'd like to get AnimatePresence to wait until Suspense resolves the Page, not transition from Page to null to Page (assume also that Page returns a motion component)

<AnimatePresence exitBeforeEnter>
  <Suspense fallback={null}>
    <Page {...pageProps} key={router.asPath}/>
  </Suspense>
</AnimatePresence>
egbadon-victor commented 1 year ago

I guess I now understand why my exit animations for my fallback component were not working

dumbravaandrei22 commented 1 year ago

Hello, I think I have the same problem. I am trying to use AnimatePresence in order to make some route navigation transitions using NextJs 13. NextJs 13 is now using Suspense and my AnimatePresence parent will wrap the NextJs tree: (my AnimatePresence is in the layout.tsx).

image

In my case, the component (page) that is about to be unmounted is not "frozen". The children (the new page) updates in both components. The old page (that is about to be unmounted) should be completely frozen.

Next13 Docs: https://beta.nextjs.org/docs/routing/fundamentals Any updates on this one?

samypogs commented 1 year ago

@dumbravaandrei22 Im also experiencing this problem. Did you find any solution or workaround to this?

dumbravaandrei22 commented 1 year ago

hey @samypogs Yes, I did.

I used react-freeze and usePresence order to accomplish this. My code under the AnimatePresence looks like this:

image
dumbravaandrei22 commented 1 year ago

But this is just a workaround. AnimatePresence should know how to handle this case. So the GitHub issue should stay open.

james-william-r commented 1 year ago

Also interested in achieving this! 🙋

joevingracien commented 1 year ago

Would be lovely to be able to natively do that :)

vimtor commented 1 year ago

This would be amazing!

tomaszbryndzia commented 10 months ago

Error still occurs. I tried dumbravaandrei22 solution but still fails.

@UPDATE My issue was a bit unrelated. But if someone ever encounter is. Key is to check if component is mounted before do any framer motion action

The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.

Main Component

    <AnimatePresence>
      {isOpen && (
        <Child animation={animation} classes={classes} key="something">
          {children}
        </Child>
      )}
    </AnimatePresence>
  );

Child Component


  const [isPresent, safeToRemove] = usePresence();

  return (
    <motion.div
      {...animation}
      transition={{ duration: 0.2 }} // Duration of the animation (in seconds)
      className={classes}
      onAnimationComplete={() => {
        if (!isPresent) safeToRemove();
      }}
    >
      <Freeze freeze={!isPresent}>
        <div>{children}</div>
      </Freeze>
    </motion.div>
  );
};```
pongells commented 7 months ago

@dumbravaandrei22 and @tomaszbryndzia -- could you create an example of your workaround in action?

does it replace using < Suspense >?

michael-mashush commented 7 months ago

I'm a newbie, faced the same problem and tried to solve it like this. The FullscreenLoader component appears suddenly to hide the content of an unloaded page, but disappears smoothly.

  1. Fallback component:
type FallbackProps = {
  onMount: () => void
  onUnmount: () => void
}

const Fallback: React.FunctionComponent<FallbackProps> = (props) => {

  React.useEffect(() => {
    props.onMount()
    return () => {
      setTimeout(props.onUnmount, 1000)
    }
  }, [])

  return null

}
  1. PageManager component:
const PageManager: React.FunctionComponent = () => {

  const [ isLoading, setIsLoading ] = React.useState<boolean>(true)

  function startLoading(): void {
    setIsLoading(true)
  }

  function endLoading(): void {
    setIsLoading(false)
  }

  useRedirectToInitialPage()

  const {
    actualRoutes,
    redirectPath,
    redirectFrom
  } = useAppRoutingInformation()

  return (
    <React.Fragment>
      <FullscreenLoader
        isVisible={isLoading}
        message="Загружаем необходимые данные, пожалуйста, подождите."
      />
      <AnimatePresence initial={false} mode="wait">
        <React.Suspense fallback={(
          <Fallback 
            onMount={startLoading} 
            onUnmount={endLoading}
          />
        )}>
          <Routes>
            <Route Component={Wrapper}>
              {actualRoutes.map((route) => (
                <Route 
                  key={route.path} 
                  path={route.path}
                  Component={route.Component}
                />
              ))}
              <Route 
                path="*" 
                element={ <Navigate to={redirectPath} state={{from: redirectFrom}} /> }
              />
            </Route>
          </Routes>
        </React.Suspense>
      </AnimatePresence>
    </React.Fragment>
  )

}
  1. FullscreenLoader component:
const FullscreenLoader: React.FunctionComponent<Props> = (props) => {

  return (
    <AnimatePresence initial={false}>
      {props.isVisible && (
        <LoaderBackdrop>
          <LoaderContent message={props.message} />
        </LoaderBackdrop>
      )}
    </AnimatePresence>
  )

}
lpic10 commented 6 months ago

I have here a working example using context: codesandbox

mattgperry commented 5 months ago

Closing this as a wontfix

I just had a spike on this. My general approach, if anyone wants to take a stab, is to have a component with the same sig as Suspense i.e

<AnimateSuspense fallback={fallback}>{children}</AnimateSuspense>

This contained a Suspense component that was provided children but instead of providing fallback we provide one that just contains an effect that fires when the component unmounts. This sets some state that removes the user-provided fallback which we've been provided by the user.

However it didn't work too well, the to fundamental issues being how do we conditionally not render fallback if the children Promise doesn't throw? And how do we know if it throws in the future? This makes it ok to animate children when its first mounted but the expectation would be that its exit prop works too, and I couldn't get that working either.