danielberndt.net
20 October 2022

I’ve tried Framer Motion. Now I’m switching back to react-spring

For the longest time I’ve been using react-spring for almost all of my animation needs in react. For a new project, I wanted to test out Framer Motion.

Whats nice about Framer Motion

  • Nice abstractions. One example would be <AnimatePresence/> which feels nicer to work with than react-spring’s useTransition.
  • Powerful features around Layout Animations that react-spring does not (aim to) offer. Not working perfectly in some more tricky situations. Debugging those requires you to have a deeper understanding of how the magic actually works under the hood.
  • Very little need to use prop-drilling to orchestrate a single transition affecting multiple components. Rather than requiring a single place to control and manage all transitions, each motion.div can define their individual behaviour.
  • Nice defaults: using springs for css-properties involving motion and easings for others. (There’s hardly ever a need to use a bouncy spring behaviour for e.g. opacity). The default animation speed is set way too slow for most of my animation needs however.

Whats not so nice about Framer Motion

  • Examples in Docs are using codesandbox.io which takes ages (10+ seconds) to load.

  • Seemingly simple things like applying pointerEvents: "none" if the opacity is less than 0.5 require you to jump surprisingly deep into the Framer Motion api:

    // taken from https://gist.github.com/mattgperry/5046aedef6bdec7f28efe3712cf3b6a8
    
    const Overlay = ({isVisible}) => {
      const opacity = useMotionValue(0);
      const pointerEvents = useTransform(opacity, (latest) =>
        latest < 0.5 ? "none" : "auto"
      );
    
      return (
        <motion.div
          animate={{opacity: isVisible ? 1 : 0}}
          style={{opacity, pointerEvents}}
        />
      );
    };

    I was somewhat baffled by this code. Who is control of the opacity value? The useMotionValue hook or the animate prop of the motion.div? Apparently the latter one.

    Here’s what it would look like with react-spring:

    const Overlay = ({isVisible}) => {
      const opacity = useSpring(isVisible ? 1 : 0);
      const pointerEvents = opacity.to((val) => (val > 0.5 ? "auto" : "none"));
    
      return <animated.div style={{opacity, pointerEvents}} />;
    };

    This API here is also a bit awkward, but that’s the price you need to pay in order to avoid additional react-rerenders in both examples. I definitely prefer the flow of control in the react-spring example though. useSpring is in charge of the opacity value and everything else is derived from it.

  • AnimatePresence isn’t as magical as I have hoped. Here’s one context in which it was harder to work with than expected: I’m working on a notification component which shows notifications in a stack. The fixed container should only be rendered if at least one message is still present. Otherwise render null.

    Here’s the code I came up with.

    const ShowMessages = ({messages}) => {
      const [shown, setShown] = useState(messages.size > 0);
    
      useEffect(() => {
        if (!shown && messages.size > 0) setShown(true);
      }, [messages.size, shown]);
    
      if (!shown && messages.size === 0) return null;
    
      return (
        <div style={{position: "fixed", bottom: 10, left: 10}}>
          <AnimatePresence
            onExitComplete={() => {
              // onExitComplete will be called for any message to exit, not just the last one
              if (messages.size === 0) setShown(false);
            }}
          >
            {[...messages.values()].map((msg, idx) => (
              <MessageComp key={msg.key} message={msg} idx={idx} />
            ))}
          </AnimatePresence>
        </div>
      );
    };

    To be fair, there’s no elegant solution using react-spring either. You’d either use two springs, or use the onDestroyed hook.

    Another instance in which AnimatePresence failed though was when using a single useMotionValue in two motion.divs. The AnimatePresence wouldn’t unmount. I had two use two useMotionValue to resolve this.

  • Certain things seem extremely hard to model. I’m working with react-router. When on /item-list, I’d like to show an edit modal when opening a route like /item-list/edit/123. The next button on this modal shows the next item. I’d like the current edit modal to fade out and the new one to fade in. It’s really, really hard to model this in Framer Motion, whereas react-spring’s useTransition is pretty much made for this and plays fairly well with the hooks react-router has to offer.

So eventually I switched back to react-spring. It’s often not as comortable, but I do feel more in control.

Suggest editGitHub