Search code examples
reactjsreact-hooksgsap

Simple animation with react and gsap


I have been struggling with this for days and am just having trouble wrapping my head around the varied documentation and examples out there, so I'm hoping someone can help with the specific example I'm working on and that might in turn help me understand this a bit better. This seems like it's far more convoluted than it should be, but I'm sure that's just my lack of understanding of how it all works.

The goal: make a very simple animation of a red ball moving back and forth. Hell, make any kind of animation at all (once I can confirm that gsap is animating anything at all, I should be able to take it from there).

The problem: Nothing happens. No errors, nothing clear to go on, just nothing. I still don't have a strong understanding of how this should work; a lot of the guides I've checked seem to be using different methods and don't go into a lot of detail as to why, so it's made it difficult to extend that knowledge to my specific scenario.

The code. I've simplified this greatly because, as I mentioned, all I really need is to just get any kind of gsap animation to work at all and from there I'm confident I can do with it what I need. If anyone feels like it would make a difference, I'm happy to update with the full code:

import React, { useRef, useEffect } from 'react';
import { gsap } from 'gsap';

const tl = gsap.timeline({paused: true, repeat: 0});

function App() {

  const waitingAnimationRef = useRef(null);

  useEffect(() => {
    tl.set(waitingAnimationRef, {autoAlpha: 0});
    tl.play();
  }, []);

  return (
    <div className="App">
      <div id="red-circle" ref={waitingAnimationRef}></div>
    </div>
  );
}

export default App;

Solution

  • Here's a working example and some tips to help you make some sense of it:

    1. Create a timeline with gasp.timeline and store it in a ref. You can do this as you've done, creating the timeline outside your component, but then you need to pass that timeline to the ref in your component. In this example, I did that by passing the variable name for the timeline to the useRef hook directly: const tl = useRef(timeline);.
    2. I used the timeline options { repeat: -1, yoyo: true } so that the animation would loop infinitely in alternating directions because you said you wanted to make a ball "moving back and forth".
    3. You'll also need a DOM node ref so you can pass that to the gsap.context() method. Create the ref in your component and pass it to the wrapping element of the component. Here, I called mine app (const app = useRef(null)) and then passed it to the top level div in the App component with ref={app}. Make sure you're passing the ref to a DOM node and not a React component (or you'll have to forward the ref down to a node child within it). Why are we using refs? Because refs are stable between rerenders of your components like state, but unlike state, modifying refs don't cause rerenders. The useRef hook returns an object with a single property called current. Whatever you put in the ref is accessed via the current property.
    4. Use a useLayoutEffect() hook instead of useEffect. React guarantees that the code inside useLayoutEffect and any state updates scheduled inside it will be processed before the browser repaints the screen. The useEffect hook doesn't prevent the browser from repainting. Most of the time we want this to be the case so our app doesn't slow-down. However, in this case the useLayoutEffect hook ensures that React has performed all DOM mutations, and the elements are accessible to gsap to animate them.
    5. Inside the useLayoutEffect, is where you'll use the gsap context method. The gsap context method takes two arguments: a callback function and a reference to the component. The callback is where you can access your timeline (don't forget to access via the current property of the ref object) and run your animations.
    6. There are two ways to target the elements that you're going to animate on your timeline: either use a ref to store the DOM node or via a selector. I used a selector with the ".box" class for the box element. This is easy and it's nice because it will only select matching elements which are children of the current component. I used a ref for the circle component. I included this as an example, so you could see how to use forwardRefs to pass the ref from the App component through Circle component to the child div DOM node. Even though this is a more "React-like" approach, it's harder and less flexible if you have a lot of elements to animate.
    7. Just like useEffect, useLayoutEffect returns a clean up function. Conveniently, the gsap context object has a clean up method called revert.
    
    import { useLayoutEffect, useRef } from "react";
    import gsap from "gsap";
    
    const timeline = gsap.timeline({ repeat: -1, yoyo: true });
    
    function App() {
      const tl = useRef(timeline);
      const app = useRef(null);
      const circle = useRef(null);
    
      useLayoutEffect(() => {
        const ctx = gsap.context(() => {
          tl.current
            // use scoped selectors
            // i.e., selects matching children only
            .to(".box", {
              rotation: 360,
              borderRadius: 0,
              x: 100,
              y: 100,
              scale: 1.5,
              duration: 1
            })
            // or refs
            .to(circle.current, {
              rotation: 360,
              borderRadius: 50,
              x: -100,
              y: -100,
              scale: 1.5,
              duration: 1
            });
        }, app.current);
    
        return () => ctx.revert();
      }, []);
    
      return (
        <div ref={app} className="App">
          <Box />
          <Circle ref={circle} />
        </div>
      );
    }