Exit animation with `framer-motion` demystified
Tags: tutorial, react, javascript, implementation
Published: 2022-06-29 19:07:23

Unlike **Svelte** which has built-in animation and transition, **React** does not. 
If you have worked with animation in React, you probably faced the problem of not being able to animate easily a component that will unmount.

```jsx
function App() {
  const [shouldShow, setShouldShow] = useState(true);

  // Do some animation when unmounting
  const onExitAnimation = ...;

  return shouldShow ? (
    <div onExit={onExitAnimation}>
      Animated when unmounting
    </div>
  ) : (
    <p>No more component</p>
  );
}
```

For example, when working with **`react-spring`**, you have to pass your state to the `useTransition` hook that will give you a new variable to use.
You can't directly condition the display of your component with the `shouldShow` state.
This way `react-spring` manages this state internally to change it when the component has finished the animation.

```jsx
function App() {
  const [shouldShow, setShouldShow] = useState(true);
  const transitions = useTransition(shouldShow, {
    leave: { opacity: 0 },
  });

  return transitions(
    (styles, show) =>
      // Here we do not use directly `shouldShow`
      show && (
        <animated.div style={styles}>
          Animated when unmounting
        </animated.div>
      )
  );
}
```

To me it doesn't feel natural.

When I finally decided to take a look at **`framer-motion`**, it was a real pleasure when I discovered the **`AnimatePresence`** component that handles it more naturally for me.

> **Note:** As always, the code I will write is simplified compared to the library code.

----

## Exit animation with `framer-motion`

Let's start by looking at the code to do such animation with **`framer-motion`**.

It's pretty simple to do this animation:

```jsx
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";

export default function App() {
  const [show, setShow] = useState(true);

  return (
    <>
      <button type="button" onClick={() => setShow(!show)}>
        Show / Unshow
      </button>
      <AnimatePresence>
        {show ? (
          <motion.p exit={{ opacity: 0 }}>
            Animated content
          </motion.p>
        ) : null}
      </AnimatePresence>
    </>
  );
}
```

> **Note:** I just configure the `exit` but you can also configure the "enter" transition thanks to `intial` and `animate` prop.

Crazy simple. But how do they manage to do this exit animation? Have you an idea? Just two words **React `ref`** :)

----

## Under the hood

### Make enter and exit animation

As you have seen in the previous example of **`framer-motion`** you can access to an object named `motion`. From it, you can get your animated elements on which you can use the props `initial`, `animate` and `exit`.

**Own implementation specification**

- make a `motion` object which has a key `p` that returns a React component to do animation
- this component have two public `props` named `onEnter` to animate when mounting and `onExit` to animate when unmounting
- use the animation web API

> **Note:** If you want to know more about the animation web API you can read my article [Let's do some animations in native Javascript](https://dev.to/romaintrotard/let-s-do-some-animations-in-native-javascript-23jl)


Let's trigger the enter and exit animation thanks to an `useEffect`. We get the following implementation for `AnimatedComponent` and `motion`:

```jsx
const AnimatedComponent =
  (Tag) =>
  ({ onExit, onEnter, ...otherProps }) => {
    const elementRef = useRef(null);

    useEffect(() => {
      const animation = elementRef.current.animate(
        onEnter,
        {
          duration: 2000,
          fill: "forwards",
        }
      );

      return () => {
        const animation = elementRef.current.animate(
          onExit,
          {
            duration: 2000,
            fill: "forwards",
          }
        );
        animation.commitStyles();
      };
      // I don't include onEnter and onExit as dependency
      // Because only want them at mount and unmount
      // Could use references to satisfy the eslint rule but
      // too much boilerplate code
    }, []);

    return <Tag {...otherProps} ref={elementRef} />;
  };

const motion = {
  p: AnimatedComponent("p"),
};
```

Unfortunately if we try this implementation the exit animation will not work :(

**Why is it complicated to do such animation?**

The reason is because when a component is no more in the React tree, it's directly removed from the DOM tree too.

**How to solve this?**

The idea is to trigger the animations thanks to a property `isVisible`.

```jsx
const AnimatedComponent =
  (Tag) =>
  ({ onExit, onEnter, isVisible, ...otherProps }) => {
    const elementRef = useRef(null);

    useEffect(() => {
      if (isVisible) {
        const animation = elementRef.current.animate(
          onEnter,
          {
            duration: 2000,
            fill: "forwards",
          }
        );

        return () => animation.cancel();
      } else {
        const animation = elementRef.current.animate(
          onExit,
          {
            duration: 2000,
            fill: "forwards",
          }
        );
        animation.commitStyles();

        return () => animation.cancel();
      }
    }, [isVisible]);

    return <Tag {...otherProps} ref={elementRef} />;
  };
```

But we do not want the user to handle the `isVisible` property. Moreover the component needs to stay in the React tree to work.

It's here that comes the `AnimatePresence` component that will keep the unmounted children in a reference and at each render detects components that are removed.

In order to do that, we need to be able to distinguish each children components. We are going to use key for that.

![Detection of removed elements](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lmx4j72jjmgnvz6kw1bh.png)
 

**Things you need to know**

- `React.Children.forEach` utility function that allows us to loop through all children
- `React.isValidElement` function that allows us to validate that we have a React element
- the `key` is at the first level of `ReactElement` and not in `props`!

----

### Get all valid children

Let's do a function to get all valid children components:

```js
function getAllValidChildren(children) {
  const validChildren = [];

  React.Children.forEach(children, (child) => {
    if (React.isValidElement(child)) {
      validChildren.push(child);
    }
  });

  return validChildren;
}
```

----

### Keep children of previous render

As I said previously, we are going to keep children of the previous render thanks to React reference.

> If you want to know more about the usage of **React ref**, you can see my article [Things you need to know about React ref](https://dev.to/romaintrotard/things-you-need-to-know-about-react-ref-25ld).

```jsx
import { useRef, useLayoutEffect } from "react";

function AnimatePresence({ children }) {
  const validChildren = getAllValidChildren(children);
  const childrenOfPreviousRender = useRef(validChildren);

  useLayoutEffect(() => {
    childrenOfPreviousRender.current = validChildren;
  });
}
```

----

### Get key of children and determine removed keys

Now let's write the method to get the key of a React element:

```js
function getKey(element) {
  // I just define a default key in case the user did
  // not put one, for example if single child
  return element.key ?? "defaultKey";
}
```

Alright, now let's get keys of the current render and of the previous one to determine which elements have been removed:

```jsx
import { useRef, useLayoutEffect } from "react";

function AnimatePresence({ children }) {
  const validChildren = getAllValidChildren(children);
  const childrenOfPreviousRender = useRef(validChildren);

  useLayoutEffect(() => {
    childrenOfPreviousRender.current = validChildren;
  });

  const currentKeys = validChildren.map(getKey);
  const previousKeys =
    childrenOfPreviousRender.current.map(getKey);
  const removedChildrenKey = new Set(
    previousKeys.filter((key) => !currentKeys.includes(key))
  );
}
```

----

### Get removed elements

Now that we get keys of element that will unmount in the current render, we need to get the matching element.

To do that the easier way is to make a map of elements by key.

```jsx
function getElementByKeyMap(validChildren, map) {
  return validChildren.reduce((acc, child) => {
    const key = getKey(child);
    acc[key] = child;
    return acc;
  }, map);
}
```

And we keep the value in a ref to preserve values at each render:

```jsx
import { useRef, useLayoutEffect } from "react";

function AnimatePresence({ children }) {
  const validChildren = getAllValidChildren(children);
  const childrenOfPreviousRender = useRef(validChildren);
  const elementByKey = useRef(
    getElementByKeyMap(validChildren, {})
  );

  useLayoutEffect(() => {
    childrenOfPreviousRender.current = validChildren;
  });

  useLayoutEffect(() => {
    elementByKey.current = getElementByKeyMap(
      validChildren,
      elementByKey.current
    );
  });

  const currentKeys = validChildren.map(getKey);
  const previousKeys =
    childrenOfPreviousRender.current.map(getKey);
  const removedChildrenKey = new Set(
    previousKeys.filter((key) => !currentKeys.includes(key))
  );

  // And now we can get removed elements from elementByKey
}
```

It's going well!

----

**What's going next?**

As we have seen at the beginning we can't do the exit animation when unmounting the component thanks to the **cleaning function** in `useEffect`.
So we will launch this animation thanks to a boolean `isVisible` that will trigger 
- the enter animation if true 
- the exit one if false.

This property will be injected to the `AnimatedComponent` by `AnimatePresence` thanks to the `React.cloneElement` API.


So we are going to change dynamically at each render the element that are displayed:
- inject `isVisible={true}` if always presents
- inject `isVisible={false}` if removed

> **Note:** We are going to introduce a new variable named `childrenToRender` to do that.

----

### Injection of `isVisible` into `AnimatedComponent`

```jsx
import { useRef, useLayoutEffect } from "react";

function AnimatePresence({ children }) {
  const validChildren = getAllValidChildren(children);
  const childrenOfPreviousRender = useRef(validChildren);
  const elementByKey = useRef(
    getElementByKeyMap(validChildren, {})
  );

  useLayoutEffect(() => {
    childrenOfPreviousRender.current = validChildren;
  });

  useLayoutEffect(() => {
    elementByKey.current = getElementByKeyMap(
      validChildren,
      elementByKey.current
    );
  });

  const currentKeys = validChildren.map(getKey);
  const previousKeys =
    childrenOfPreviousRender.current.map(getKey);
  const removedChildrenKey = new Set(
    previousKeys.filter((key) => !currentKeys.includes(key))
  );

  // We know that `validChildren` are visible
  const childrenToRender = validChildren.map((child) =>
    React.cloneElement(child, { isVisible: true })
  );

  // We loop through removed children to add them with
  // `isVisible` to false
  removedChildrenKey.forEach((removedKey) => {
    // We get the element thanks to the object
    // previously builded
    const element = elementByKey.current[removedKey];
    // We get the index of the element to add it
    // at the right position
    const elementIndex = previousKeys.indexOf(removedKey);

    // Add the element to the rendered children
    childrenToRender.splice(
      elementIndex,
      0,
      React.cloneElement(element, { isVisible: false })
    );
  });

  // We don't return `children` but the processed children
  return childrenToRender;
}
```

{% codesandbox framer-motion-animatepresence-quite-working-hvy1ys %}

Oh wouah! 
The animation works now but it's not totally perfect because the element stays in the tree. We need to re-render the `AnimatePresence` when all exit animation has been done.

We can know when an animation is ended thanks to the `animation.finished` promise.

----

### `useForceRender` hook

The `useForceRender` hook can be done with a simple counter:

```js
import { useState, useCallback } from "react";

function useForceRender() {
  const [_, setCount] = useState(0);

  return useCallback(
    () => setCount((prev) => prev + 1),
    []
  );
}
```

----

### Re-render when all exit animation are done

The final step is to re-render the `AnimatePresence` component when all the exit animation are finished to render the right React elements.

After this triggered render, there will be no more the removed element in the React tree.

```jsx
import { useRef, useLayoutEffect } from "react";

function AnimatePresence({ children }) {
  const forceRender = useForceRender();
  const validChildren = getAllValidChildren(children);
  const childrenOfPreviousRender = useRef(validChildren);
  const elementByKey = useRef(
    getElementByKeyMap(validChildren, {})
  );

  useLayoutEffect(() => {
    childrenOfPreviousRender.current = validChildren;
  });

  useLayoutEffect(() => {
    elementByKey.current = getElementByKeyMap(
      validChildren,
      elementByKey.current
    );
  });

  const currentKeys = validChildren.map(getKey);
  const previousKeys =
    childrenOfPreviousRender.current.map(getKey);
  const removedChildrenKey = new Set(
    previousKeys.filter((key) => !currentKeys.includes(key))
  );

  const childrenToRender = validChildren.map((child) =>
    React.cloneElement(child, { isVisible: true })
  );

  removedChildrenKey.forEach((removedKey) => {
    const element = elementByKey.current[removedKey];
    const elementIndex = previousKeys.indexOf(removedKey);

    const onExitAnimationDone = () => {
      removedChildrenKey.delete(removedKey);

      if (!removedChildrenKey.size) {
        forceRender();
      }
    };

    childrenToRender.splice(
      elementIndex,
      0,
      React.cloneElement(element, {
        isVisible: false,
        onExitAnimationDone,
      })
    );
  });

  return childrenToRender;
}
```

And the `AnimateComponent` finally becomes:

```jsx
const AnimatedComponent =
  (Tag) =>
  ({
    onExit,
    onEnter,
    isVisible,
    onExitAnimationDone,
    ...otherProps
  }) => {
    const elementRef = useRef(null);

    useEffect(() => {
      if (isVisible) {
        const animation = elementRef.current.animate(
          onEnter,
          {
            duration: 2000,
            fill: "forwards",
          }
        );

        return () => animation.cancel();
      } else {
        const animation = elementRef.current.animate(
          onExit,
          {
            duration: 2000,
            fill: "forwards",
          }
        );
        animation.commitStyles();
        // When the animation has ended
        // we call `onExitAnimationDone`
        animation.finished.then(onExitAnimationDone);

        return () => animation.cancel();
      }
    }, [isVisible]);

    return <Tag {...otherProps} ref={elementRef} />;
  };
```

And here we go!

{% codesandbox framer-motion-animatepresence-own-implem-t33st0 %}

----

## Conclusion

I hope I've managed to make you understand how it all works under the hood.
Actually the real implementation is not the same that I have done. They do not `cloneElement` but use the React context API to be able not to pass directly an animated component (`motion.something`).
But the main point to remember is the usage of **references** to get children of previous render and that the returned JSX is something processed by the `AnimatePresence` that manages the animation of its children and more specifically the exit one by delaying the unmount of components to see the animation. 

If you have any question do not hesitate to ask me.

----

Do not hesitate to comment and if you want to see more, you can follow me on [Twitch](https://twitch.tv/rainbowapps ) or go to my [Website](https://www.romaintrotard.com).