4

I'm trying to add some animations and smoothness to an app with Framer-motion and I'm struggling to make it all work.

Using react-router 6, I want to trigger some exit animations on route sub-components when the url changes. Following this tutorial, here is what I got for the main layout :

export default function MainWrapper() {
  const location = useLocation();
  return (
    <Main>
      <AnimatePresence exitBeforeEnter initial={false}>
        <Routes key={location.pathname}>
          <Route path="/" element={<Dashboard />} />
          <Route path="project/:id/*" element={<Project />} />
        </Routes>
      </AnimatePresence>
    </Main>
  );
}

The pages Dashboard and Project are build using some composition of Antd's Row and Col system. I want to animate Row's children to appear one after the other on mount, and to disappear one after the other on unmount :

import React, { ReactNode } from "react";
import { Card, Col, Row } from "antd";
import { motion } from "framer-motion";

// Section

type SectionProps = {
  title?: ReactNode;
  extra?: ReactNode;
  children?: ReactNode;
  span?: number;
};

export default function Section({
  title,
  extra,
  children,
  span = 24
}: SectionProps) {
  return (
    <MotionCol span={span} variants={colVariant}>
      <Card title={title} extra={extra}>
        {children}
      </Card>
    </MotionCol>
  );
}

// Section.Group

type GroupProps = {
  children?: ReactNode;
};

Section.Group = function({ children }: GroupProps) {
  return (
    <MotionRow
      gutter={[24, 24]}
      variants={rowVariant}
      initial="hidden"
      animate="show"
      exit="close"
    >
      {children}
    </MotionRow>
  );
};

// Framer stuff

const MotionRow = motion(Row);
const MotionCol = motion(Col);

const transition = { duration: 0.4, ease: [0.43, 0.13, 0.23, 0.96] };

const rowVariant = {
  hidden: {},
  show: {
    transition: {
      staggerChildren: 0.1
    }
  },
  close: {}
};

const colVariant = {
  hidden: { opacity: 0, x: 20, transition },
  show: { opacity: 1, x: 0, transition },
  close: {
    opacity: 0,
    x: -20,
    transition
  }
};

Dashboard is then built using these blocks :

<Section.Group>
  <Section>
    First section...
  </Section>
  <Section>
    Second section...
  </Section>
</Section.Group>

The issue : Only hidden and show work. Not close. There is no exit-animation when leaving a page. How could I solve this ? Thank you.

ostrebler
  • 933
  • 3
  • 19

2 Answers2

2

Things I found wrong:

  1. For custom components the motion function requires you to forward the ref

Docs: https://www.framer.com/api/motion/component/#custom-components

const ForwardedAntdRow = React.forwardRef((props, ref) => (
  <Row ref={ref} {...props} />
));
const MotionRow = motion(ForwardedAntdRow);

not

const MotionRow = motion(Row);
  1. Route doesn't have an element prop
  <Route path="/page1">
    <Page1 />
  </Route>

is pretty standard notation as far as I know (I don't work with react-router often)

I created a working example here: https://codesandbox.io/s/framer-motion-animate-react-router-transition-kczeg?file=/src/index.js:1308-1372

I can answer any other questions you have when I am online tomorrow. Let me know

Joshua Wootonn
  • 326
  • 1
  • 6
  • Thanks for your answer. Two elements here : 1) Antd's `Row` automatically forwards ref as far as I know, since motion works with it just fine, except for exit animations. 2) React-router 6 has an `element` prop on `Route`. – ostrebler May 07 '21 at 22:28
  • 1
    1) I am not super familiar with antd's grid, but adding the ref forward is what makes the exit animation work in my code sandbox, you can try it for yourself by removing it. 2) Nice ok, I guess I don't totally understand when you need to include the base react-router / what the advantage of it over react-router-dom – Joshua Wootonn May 08 '21 at 18:25
1

I can see two potential problems with your code:

1.

Note: Child motion components must each have a unique key prop so AnimatePresence can track their presence in the tree.

Note: The custom component being removed from the DOM must still be a direct descendant of AnimatePresence for the exit animation(s) it contains to trigger.

source: https://www.framer.com/api/motion/animate-presence/

So your code would become:

export default function MainWrapper() {
  const location = useLocation();
  return (
    <Main>
      
      <Routes key={location.pathname}>
        <AnimatePresence exitBeforeEnter initial={false}>
          <Route key="dashboard" path="/" element={<Dashboard />} />
          <Route key="project" path="project/:id/*" element={<Project />} />
        </AnimatePresence>
      </Routes>

    </Main>
  );
}
gkpo
  • 2,093
  • 2
  • 22
  • 36
  • This does not work either. What I don't understand is that it seems like I've the same code than in the tutorial I linked – ostrebler May 02 '21 at 04:14