Prompted by a one of my students who was having trouble implementing a GSAP animation in React I decided to experiment a little and write up what I learned.
If you are unfamiliar with useState you can check out my other blog post here
If you are unfamiliar with GreenSock you can check out my blog post on getting started here
I'll say off the bat that I am still experimenting with this and learning the best practices for Hooks and GreenSock. If you have any suggestions for improved code leave them in the comments!
This is not a full tutorial of the whole project just an overview of how I added GreenSock and implemented it with Hooks. If you would like to just see the code you can check it out below 👇
This project uses styled components. If you want to know more check out the docs here
The first thing I did for this project was import in the hooks I would be using.
import React, { useRef, useEffect, useState } from "react";
I also made sure I had GSAP added as a dependency and imported it as well.
import { TweenMax, TimelineMax,Elastic, Back} from "gsap";
TweenMax, TimeLineMax, Elastic, and Back are all parts of GreenSock that I used in my animations so I needed to import each module.
TweenMax and TimeLineMax are used to create the animations.
Elastic and Back are types of easing I used in the animations.
These will be changing soon with the new GSAP v3. I'll try to update this post when the new GreenSock API drops but even so you will still be able to use the current syntax I am using with GSAP v3.
If you want to check out more easing I highly suggest looking at this ease visualizer when creating animations.
Ease Visualizer
useRef
"The useRef hook is primarily used to access the DOM, but it’s more than that. It is a mutable object that persists a value across multiple re-renderings. It is really similar to the useState hook except you read and write its value through its .current property, and changing its value won’t re-render the component."
Hunor Márton Borbély CSS-Tricks
The key to animating things in React with GreenSock is to make sure you get a reference for the element you want to animate. To grab a reference to the elements we want to animate we can use the useRef hook.
For our cards we will be animating the image, some hidden text and our actual card. I set up the refs like this:
let imgRef = useRef(null);
let textRef = useRef(null);
let cardRef = useRef(null);
I am mapping through a bunch of data to spit out my cards here so I am using let instead of const in this instance since the img, text, and card reference will change depending on the card.
Next I needed to add the references to the components.
<Card
onMouseEnter={() => mouseAnimation.play()}
className="dog-card "
key={props.id}
ref={element => {
cardRef = element;
}}>
<DogImage
ref={element => {
imgRef = element;
}}
className="dog-image"
alt="random dog"
src={props.imgUrl}
/>
<RevealH3
ref={element => {
textRef = element;
}}
className="reveal"
>
Thank you!
<span role="img" aria-label="triple pink heart">💗</span>
</RevealH3>
<DogButton
onClick={() => clickAnimation.play()}
>
AdoptMe
</DogButton>
<MainTitle>{props.breed}</MainTitle>
</Card>
);
};
I am using callback refs here.
Here is an except from the GreenSock docs on refs by Rodrigo:
"Keep in mind that the ref is a callback that, used as an attribute in the JSX code, grabs whatever is returned from the tag where is used but is a function, now you're only referencing that function but you're not doing anything with it. You have to create a reference to the DOM element in the constructor and then use the callback to update it at render time"
For my functional component I created references to the DOM elements I want to animate with useRef. Then I add the callback refs in my JSX.
Like this one:
<RevealH3
ref={element => {
textRef = element;
}}
className="reveal"
>
Now that I have access to the DOM elements with the useRef hook I can animate the elements the same way I normally would in GreenSock. The only difference here is I will be putting the animation in a useEffect hook and setting our initial animation states in the useState hook.
We use useState anytime we have data in a component we want to update. In this app I am updating several animations so I added them to state
Setting up our State
const [mouseAnimation, setMouseAnimation] = useState();
const [clickAnimation, setClickAnimation] = useState();
const [tl] = useState(new TimelineMax({ paused: true }));
We will set our setMouseAnimation and setClickAnimation in the useEffect hooks. They will will be updated with events in our JSX.
Per the React Docs I am separating out my animations in to different useEffect hooks instead of one. As far as I could find this should be best practice.
First animation
useEffect(() => {
setMouseAnimation(
TweenMax.to(imgRef, 1, {
scale: 1,
filter: "none",
ease: Elastic.easeOut.config(1, 0.75)
}).pause()
);
},[])
This is grabbing the reference to our img. I chained the .pause() method to the tween so that it will only run when we set up our event.
Below I add the animation to an onMouseEnter event and chain the .play() method to it so it runs when the mouse enters the card.
<Card
onMouseEnter={() => mouseAnimation.play()}
className="dog-card "
key={props.id}
ref={element => {
cardRef = element;
}}>
Second Animation
For this animation I used GreenSock's TimelineMax. I set the initial state of the timeline with the useState Hook.
const [tl] = useState(new TimelineMax({ paused: true }));
This sets the initial state as paused.
Then I added the animations to a useEffect hook.
useEffect(() => {
setClickAnimation( . // here we are set are state to the timeline
tl.add("s"),
tl
.to(
textRef,
1,
{
autoAlpha: 1,
y: 0,
ease: Elastic.easeIn.config(1, 0.75)
},
"s"
)
.to(
cardRef,
0.4,
{
transformOrigin: "center center",
ease: Back.easeIn.config(1.4),
scale: 0.1
},
"s+=1.5"
)
.to(
cardRef,
0.4,
{
opacity: 0,
display: "none"
},
"s+=2"
)
);
}, [tl]);
Note that for this animation I needed to add the state to our dependency array. Since we will be updating the state with an event we need to update the useEffect hook when we update our state.
This animation is referencing both the hidden text I have and our card. When the animation starts I am revealing the text. Then the card scales down and disappears. The animation is triggered with an onClick handler that is on the "Adopt me" button.
<DogButton
onClick={() => clickAnimation.play()}
>
In the onClick event we are updating our clickAnimation state to play instead of it's initial state of paused.
Now we should have 2 working animations. The first is triggered when we mouseover the card and the second when the Adopt Me button is clicked.
Top comments (3)
Great article.
I have one question. Suppose you want to pass parameters dynamically to the tl timeline, how would you do that?
Do you have an example? Typically if I am adding some kind of parameter I create a function for my TL and pass in parameters there. For example this is an animation I eventually used in a Vue app. If you go down to the startConfetti function you will see I used element and then passed in the elements I wanted to animate in the actually main timeline.
codepen.io/cgorton/pen/81813f8b48b...
Is that the kind of thing you are asking?
Am I correct in thinking there is nothing to clean up when this component is unmounted seeing as we've stored the timeline in state? Great article.