DEV Community

loading...
Cover image for Animating React: GreenSock and React Hooks

Animating React: GreenSock and React Hooks

Christina Gorton
Developer Advocate, Technical Writer, and Instructor. Courses on LinkedIn, Egghead, Design+Code, and Skillshare. https://egghead.io/instructors/christina-gorton/?af=1c6fpu
Updated on ・5 min read

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 👇

The Code

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"; 
Enter fullscreen mode Exit fullscreen mode

I also made sure I had GSAP added as a dependency and imported it as well.

import { TweenMax, TimelineMax,Elastic, Back} from "gsap";
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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"
      >
Enter fullscreen mode Exit fullscreen mode

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 }));
Enter fullscreen mode Exit fullscreen mode

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()
    );
  },[])
Enter fullscreen mode Exit fullscreen mode

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;
      }}>
Enter fullscreen mode Exit fullscreen mode

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 }));
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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()}
      >
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (3)

Collapse
gilbertmizrahi profile image
GilbertMizrahi

Great article.
I have one question. Suppose you want to pass parameters dynamically to the tl timeline, how would you do that?

Collapse
coffeecraftcode profile image
Christina Gorton Author

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?

Collapse
jai_type profile image
Jai Sandhu

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.