In this post we will go through creating a function based React component utilizing hooks to create a reusable component that simplifies transition animations. By the end of this post, we will have a basic component that will display a slide transition on the change of child objects like this:

We will be going through the basic app setup and iterative process of building this component; however, if you would like to skip to the finished result click here.
Basic App
To start, lets define a basic React app that displays a card and allows the user to cycle through the available displayed cards by clicking a next button.
This app example has two basic components, a Card that displays a title and text, and a Container that handles the display of individual cards and handles navigation to the next card based on a button click.
function CardDisplay(props){
return <div className={"card"}>
<span className={"title"}>{props.title}</span>
<p>{props.text}</p>
</div>;
}
function MainDisplay(props){
const [cardPos, setCardPos] = useState(0);
var currentCard = <CardDisplay title={props.cards[cardPos].title} text={props.cards[cardPos].text}/>;
var nextButton = <button onClick={(event)=>{
var nextPos = cardPos + 1;
if(nextPos >= props.cards.length){
nextPos = 0;
}
setCardPos(nextPos);
}}>
{"Next"}
</button>;
return <div>
{currentCard}
{nextButton}
</div>;
}
Now that we have this basic app example, lets see what it looks like.
Goals for the Animation
This animation should slide the next card into place from the right hand side and slid the old card out to the left. Although this is a rather simple animation, we want as little change to the main components as possible. In order to do this, and to make the logic modular, this implementation will use a custom hook to handle the rendering and animation control. Now that we have a basic concept of the goal of this hook, lets get started.
Basic React Component Hook Design
For this to work properly, lets first create a basic react componet that will be our animated container. At it's most basic, this will return a div containing the child elements passed to it.
function AnimationComponent(props){
return <div>{props.children}</div>;
}
When implemented, every time the button is pressed, the react component will send the next card to the AnimationContainer which will render our basic div with that card in it. Now that we have a basic container, we need the contents of that AnimationContainer to be animated on child change, this is where our custom hook comes in. Lets do a quick outline of what this hook will do and when an animation needs to take place.
- The animation will trigger on a change to the children passed to the animated container
- The animation process requires both the prior and new elements to be displayed
- After the animation cycle is complete, we need to remove the prior element from our display entirely.
Now that we have the basic requirements, lets start writing and implementing the hook.
function useAnimationState(children){
const [animatedItems, setItems] = useState(null);
useEffect(()=>{
var newState = animatedItems.concat([children]);
setItems(newState);
}, [children]);
return animatedItems;
}
function AnimationComponent(props){
var animatedItems = useAnimationState(props.children);
return <div className={"animatedContainer"}>
{animatedItems}
</div>
}
This hook now listens for a change to the child elements and add the new item to an array of all past items. Our behavior now looks like this.

At this point, we are properly listening to the child change event and displaying more than 1 child element without any change to our core components. Now we need to trigger the animations and clean up after the animation is complete.
Lets start with styling and animations. Animating the transition for this case will require two basic classes and an animation keyframe definition. Lets start by making the container display the cards next to each other.
.animatedContainer {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
Next we need to add or class and keyframe animation for our prior/leaving element.
@keyframes animateSlideOut {
0% {
margin-left: 0px;
}
100% {
margin-left: -325px;
}
}
.slideOut {
animation: animateSlideOut 0.8s;
}
Now lets update our hook to include the following:
- assigning our new css class for animating the prior element
- add an animation end event to the prior element
- once the prior element animation has completed, remove that item from the render
function useAnimationState(children){
const [animatedItems, setItems] = useState(null);
function animationEnd(){
// update the animatedItems to remove the first item
setAnimatedItems(itemRef.current.slice(1));
}
useEffect(()=>{
if(animatedItems == null){
// initial render of the component does not need a transition animation
setAnimatedItems([children]);
} else {
// on updates to the children, updated the animated chilren array to contain the new element
// the first element in the array should be wrapped with the slide out animation
setAnimatedItems([<div className={"slideOut"} onAnimationEnd={animationEnd}>{animatedItems[0]}</div>].concat([children]));
}
}, [children])
return animatedItems;
}
function AnimationComponent(props){
var animatedItems = useAnimationState(props.children);
return <div className={"animatedContainer"}>
{animatedItems}
</div>
}
With these basic updates we are starting to get a smooth animation that looks like this.

This looks much better, but we can still add some additional animations keyframe properties to the new element to make the entry animation a little smoother.
@keyframes animateFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fadeIn {
animation: animateFadeIn 0.8s;
}
Now that we have a fade in animation defined, lets update the hook to use it on new items coming in.
function useAnimationState(children){
const [animatedItems, setItems] = useState(null);
const itemRef = useRef(animatedItems)
var refUpdateProxy = {
apply: (target, thisArg, argumentsList) =>{
itemRef.current = argumentsList[0];
return Reflect.apply(target, thisArg, argumentsList);
}
}
const setAnimatedItems = new Proxy(setItems, refUpdateProxy)
function animationEnd(){
// update the animatedItems to remove the first item
setAnimatedItems(itemRef.current.slice(1));
}
useEffect(()=>{
if(animatedItems == null){
// initial render of the component does not need a transition animation
setAnimatedItems([children]);
} else {
// on updates to the children, updated the animated chilren array to contain the new element
// the first element in the array should be wrapped with the slide out animation
setAnimatedItems([<div className={"slideOut"} onAnimationEnd={animationEnd}>{animatedItems[0]}</div>].concat([<div className={"fadeIn"}>{children}</div>]));
}
}, [children])
return animatedItems;
}
Now that both elements are animated, lets take a look again at our transition.

We are now seeing some issues with the entry element cutting in and out some. The most likely cause of this issue has to due with the React element reconciliation. Basically on completion of the render cycle that removes the old element, react thinks that the new array to display has no direct association with the previously rendered frame, this causes unnecessary unmounting and mounting of the DOM element instead of just updating the item. We can fix this by letting react now the elements relation using the React key property. To do this, lets start by adding a key property to each card in our main component.
function MainDisplay(props){
const [cardPos, setCardPos] = useState(0);
var currentCard = <CardDisplay title={props.cards[cardPos].title} text={props.cards[cardPos].text} key={cardPos}/>;
var nextButton = <button onClick={(event)=>{
var nextPos = cardPos + 1;
if(nextPos >= props.cards.length){
nextPos = 0;
}
setCardPos(nextPos);
}}>
{"Next"}
</button>;
return <div>
<AnimationComponent>{currentCard}</AnimationComponent>
{nextButton}
</div>;
}
Next, we need to update the hook to make sure our component structure does not change between render cycles, and that we implement a key on each animated div based on this card key.
function useAnimationState(children){
const [animatedItems, setItems] = useState(null);
const itemRef = useRef(animatedItems)
const setAnimatedItems = (newState)=>{
itemRef.current = newState;
return setItems(newState);
}
const priorItem = useRef();
function animationEnd(){
// update the animatedItems to remove the first item
setAnimatedItems(itemRef.current.slice(1));
}
useEffect(()=>{
var key = `${children.key}_animated`;
if(animatedItems == null){
// initial render of the component does not need a transition animation
setAnimatedItems([<div key={key}>{children}</div>]);
priorItem.current = children;
} else {
// on updates to the children, updated the animated chilren array to contain the new element
// the first element in the array should be wrapped with the slide out animation
var priorItemKey = `${priorItem.current.key}_animated`;
setAnimatedItems([<div key={priorItemKey.current} className={"slideOut"} onAnimationEnd={animationEnd}>{priorItem.current}</div>].concat([<div key={key} className={"fadeIn"}>{children}</div>]));
}
}, [children])
return animatedItems;
}
Completed Component
We now have a completed slide animation component that handles slide transitions between changes. You can see the example animation and completed code in the Codepen below on the Github repo.
