3

I'd like to build a re-usable animation component with react-spring. Here's what I have so far:

codesandbox

const Fade = ({
  show,
  as = "div",
  children
}: {
  show: boolean;
  as?: keyof JSX.IntrinsicElements;
  children: React.ReactNode;
}) => {
  const transitions = useTransition(show, null, {
    from: { position: "absolute", opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 },
    unique: true
  });

  const AnimatedComponent = animated[as];

  return (
    <>
      {transitions.map(({ item, key, props }) => {
        if (!item) {
          return null;
        }

        return (
          <AnimatedComponent key={key} style={props}>
            {children}
          </AnimatedComponent>
        );
      })}
    </>
  );
};

However, there's now an issue where the animation components introduces a "side-effect", adding a "div" wrapper around the child I want animated. This causes styling issues in existing components that would require lots of changes.

So as a workaround, I attempted to use animated as a function, and pass children as a non-instantiated react element. But then there's noticeable jank and animation doesn't complete and stops mid-way through at times, e.g., inspect the animated element, and notice that the opacity tends to stop at 0.98883393 rather than at 1.

codesandbox

const Fade = ({
  show,
  children
}: {
  show: boolean;
  children: React.ReactElement;
}) => {
  const transitions = useTransition(show, null, {
    from: { position: "absolute", opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 },
    unique: true
  });

  // Here's the change. We wrap children in a functional component and clone
  // children inside
  const AnimatedComponent = animated(({ style }) =>
    React.cloneElement(children, { style })
  );

  return (
    <>
      {transitions.map(({ item, key, props }) => {
        if (!item) {
          return null;
        }

        return (
          <AnimatedComponent key={key} style={props}>
            {children}
          </AnimatedComponent>
        );
      })}
    </>
  );
};

I've noticed that introducing extra "div" wrappers seems to be a side-effect with some of these animation-based libraries like react-spring and framer-motion

Is there a suggested way to build re-usable animation components with react-spring that doesn't come with the side-effect of introducing extra DOM elements

jchi2241
  • 2,032
  • 1
  • 25
  • 49

1 Answers1

1

This is an old version of react-spring and things have changed since then so it's hard to troubleshoot exactly why you have this odd behaviour with opacity that doesn't reach 1.0. Nonetheless, you also have a warning in the console about updating an unmounted component and the key property is always undefined so I'm not sure you're using transitions correctly. But since that api documentation is no longer around, it's hard to troubleshoot.

Nonetheless, I CAN tell you that it is not necessary to add an extra wrapper element around your animated content. In the first case, you're explicitly telling react-spring to add an element (animated.XXX) which it of course does. In your second (broken) example, there is no wrapper and I think you could get that to work if you looked into how to use useTransition in that old version.

You should know however that react-spring, when at its best, uses a ref to update elements outside of React. That's why all the basic HTML elements supplied by animated(animated.div, animated.span etc...) have this wrapper behaviour that you have seen. If you use animated to wrap your own component, you basically have two choices:

  • Make sure your custom component can hold a ref and supply any given ref from animated to it (use forwardRef). Now your component will not rerender all the time but only once, and updates will be handled outside of React. With this method YOU are given the possibility to determine which element you want to give the ref to (instead of react-spring using a wrapper to simplify this for you).
  • Don't make your custom component take a ref and accept that your component will be rerendered once on every animation frame (this is what happens in your example, log each render to verify this). This is suboptimal from a performance point of view, but would still work.

So to sum up, this doesn't explain the oddity with the opacity, but it does explain why react-spring prefers to have a wrapper component to work with, because then it can guarantee that it can animate all the content of that element in an efficient way. Luckily enough, react-spring also gives you the option to choose the element type you want.

fast-reflexes
  • 4,891
  • 4
  • 31
  • 44