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 composechildrenToRender
using an iteratortick
; - While composing
childrenToRender
we will check if thechild
has to be animatedchild index === tick
, or returned as it ischild index < tick
or not returned at all; - After updating
childrenToRender
we will incrementtick
and repeat everything again until we gone throuh all thechildren
; - Last, but not least, after incrementing
tick
we check fortick === children length
to see if all the elements here handled and callonFinishCallback
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
.