DEV Community

Laimonas K
Laimonas K

Posted on

Animations as React components #2 - stacking elements

In my previous post, I suggested having animations as a separate component. Now I would like to share a bit more complex use case, which I encountered, when our design team wanted to "spice things up" - have a sidebar stack its elements one after another.

Setup

So the goal is clear - render each element one after another and apply animation when "adding" the elements. To make this as simple and as reusable as possible I wanted to have a separate component which handles all of the involved logic. Nothing ground breaking here. This component should handle:

  • Rendering the component one by one;
  • Apply animation to the lastest "added" elements;
  • Have a callback after all the elements have finished to handle some application state changes.
<Composer
  shouldRender={state}
  transition={FadeIn}
  transitionDuration={300}
  onFinishCallback={enableButton}
>
  <Element>Element 1</Element>
  <Element>Element 2</Element>
  <Element>Element 3</Element>
  <Element>Element 4</Element>
  <Element>Element 5</Element>
</Composer>

Composer

All of the requirements listed above can be easily achieved using only a few useState and useEffect hooks:

  • To have a stacking effect, we will need to map children and compose childrenToRender using an iterator tick;
  • While composing childrenToRender we will check if the child has to be animated child index === tick, or returned as it is child index < tick or not returned at all;
  • After updating childrenToRender we will increment tick and repeat everything again until we gone throuh all the children;
  • Last, but not least, after incrementing tick we check for tick === children length to see if all the elements here handled and call onFinishCallback if it is available.
const Composer = ({
  children,
  shouldRender,
  transition: Transition,
  transitionDuration,
  onFinishCallback
}) => {
  /** Track which element should be animated */
  const [tick, setTick] = React.useState(-1);
  /** Stores children with animation to be rendered */
  const [childrenToRender, setChildrenToRender] = React.useState([]);

  /** Checks the passed props and starts iterating */
  React.useEffect(() => {
    if (shouldRender) {
      setTick(tick + 1);
    } else {
      setTick(-1);
      setChildrenToRender([]);
    }
  }, [shouldRender]);

  /** Iterates over children and adds animation if required */
  React.useEffect(() => {
    const updatedChildren = children.map((child, index) => {
      return index === tick ? (
        <Transition
          key={`animated-child-${index}`}
          duration={transitionDuration}
        >
          {child}
        </Transition>
      ) : index < tick ? (
        child
      ) : null;
    });

    /** Filters null children, to make prevent unnecessary iterations */
    setChildrenToRender(updatedChildren.filter(child => !!child));
  }, [tick]);

  /** Handles calling onFinishCallback */
  React.useEffect(() => {
    if (shouldRender && tick === children.length) {
      onFinishCallback && onFinishCallback();
    }
  }, [tick]);

  /** Checks if it is required to continue iterating over children  */
  React.useEffect(() => {
    if (shouldRender && tick < children.length) {
      setTimeout(() => {
        setTick(tick + 1);
      }, transitionDuration);
    }
  }, [childrenToRender]);

  return childrenToRender;
};

Animation component

With the Composer set up, all that remains is the animation component.
Here I suggested using .attrs to set transition and transform styles based on passed state. This setup is quite good, when you need to easily handle transitions in both directions and also be able to have intermediate transitions (e.g. when reverse animation is trigered before the finishing the initial).
In this case, it was not required so I have decided to go with keyframes as this removes the need to handle state changes for each element in the Composer and is a bit more straight forward.

import styled, { keyframes } from "styled-components";

const getTransform = () => keyframes`
  from {
    transform: translateY(200px);
  }
  to {
    transform: translateY(0);
  }
`;

const getOpacity = () => keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`;

export default styled("div")`
  animation: ${getTransform()}, ${getOpacity()};
  animation-duration: ${({ duration }) => `${duration}ms`};
`;

Results

Here are a few examples of the setup in action with a few different transitions.

Top comments (0)