Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
paulorero

Dynamic animations with React props and state

Recommended Posts

Hi there,

 

TL;DR

  • How to prevent the gsap animations to be aborted in react because of a rerender/prop change
  • How to use dynamic props/state inside a gsap tween without beeing interrupted

 

I'm building a spinng wheel with dynamic start- and end-points. You guys already helped me a lot with this:

 

Now I need to implement this into an acutal react application. The biggest problem is, that I use dynamic Properties inside my gsap tweens to calculate e.g. the stop position or when to stop animation.

React rerenders my component and aborts the whole animation as soon as a property changes. Of course react should do that - but how to I keep my animation running?

 

What my code should do:

  • start spinning by clicking the "Start Spinning" Button
  • Wheel is spinning infinite
  • Stop wheel by clicking the "Stop Spinning" Button
  • Wheel at least the minimum amount (5) and then stops at the set position

 

What my code actually does:

  • start spinning by clicking the "Start Spinning" Button
  • Wheel is spinning infinite
  • Clicking "Stop Spinning" does not work ->
    • triggers in my local invironment a rerender and aborts the animation
    • in codepen it flickers and then nothing happens (the stop position is never passed into the tween)
  • ...

 

In the codepen it actually does not rerender but the updated prop won't be passed into the tween.

  const loopAnim = gsap.to(circleRef.current, {
    rotation: "+=360",
    ease: "none",
    duration: 0.5,
    onComplete: () => {
      // The props won't update in here...
      if (loopIteration.current >= fullSpins && typeof stopAt === "number") {
      stopAnim.play();
    } else {
      loopIteration.current++;
      loopAnim.play(0);
    },
    paused: true
  });

 

See the Pen ZEQpMwY by paulborm (@paulborm) on CodePen

Link to comment
Share on other sites

 

4 minutes ago, ZachSaucier said:

If I'm understanding you correctly you can just add an else in your useEffect and play the stop animation:

 

With this solution the wheel won't stop smoothly as it is supposed to be.

 

The wheel needs to start spinning, spin infite until it gets a stop position and then stop smoothly on the provided position.

Just like this but in React:

See the Pen XWmygvB by paulborm (@paulborm) on CodePen

Link to comment
Share on other sites

One alternative is to add the GSAP instance to the component's state in order to prevent re-renders from dumping the animation, like this sample:

https://codesandbox.io/s/gsap-toggle-instance-with-hooks-t9uqr

 

Another alternative, something that I'd do, is to use a class component for this one, but that is just my opinion on the subject. I know hooks are the stuff all the cool kids are using and imposing, but sometimes a class component is the easiest solution IMHO.

 

Happy Tweening!!!

  • Like 2
Link to comment
Share on other sites

Quote

Another alternative, something that I'd do, is to use a class component for this one, but that is just my opinion on the subject. I know hooks are the stuff all the cool kids are using and imposing, but sometimes a class component is the easiest solution IMHO.

I agree with you. Unfortunately I am not able to use a class component in this case.

 

Adding the GSAP instances into the state doesn't really work for me (maybe I did something wrong).

Setting tweens which include the dynamic props, requires the useEffect to include them in it's dependency array. Which then leads to a lot of rerenders, calulation and loops.

 const [spinning, setSpinning] = React.useState(false);

  const [stopTween, setStopTween] = React.useState(null);
  const [startTween, setStartTween] = React.useState(null);
  const [loopTween, setLoopTween] = React.useState(null);

  const circleRef = React.useRef(null);
  const loopIteration = React.useRef(0);
  const stopRotation = 360 + stopAt;

  const handleStartSpinning = () => {
    setSpinning(true);
  };

  React.useEffect(() => {
    setStopTween(
      gsap.to(circleRef.current, {
        rotation: `+=${stopRotation}`,
        // Calculate the new duration ...
        // ... to make the transition between spinning and ...
        // ... stopping as smooth as possible.
        duration: stopRotation / 360,
        paused: true,
      })
    );
  }, [stopRotation]);

  React.useEffect(() => {
    setLoopTween(
      gsap.to(circleRef.current, {
        rotation: "+=360",
        ease: "none",
        duration: 0.5,
        onComplete: () => {
          // The props won't update in here...
          if (
            loopIteration.current >= fullSpins &&
            typeof stopAt === "number"
          ) {
            stopTween.play();
          } else {
            loopIteration.current++;
            loopTween.play(0);
          }
        },
        paused: true,
      })
    );
  }, [stopTween, loopTween, fullSpins, stopAt]);

  React.useEffect(() => {
    setStartTween(
      gsap.to(circleRef.current, {
        rotation: "+=360",
        ease: "power1.in",
        duration: 1,
        onComplete: () => loopTween.play(),
        paused: true,
      })
    );
  }, [loopTween]);

  React.useEffect(() => {
    if (spinning) {
      startTween.play();
    }
  }, [spinning, startTween]);

 

Link to comment
Share on other sites

Mhhh... Can you set up a Codesandbox with a minimal sample in order to take a look at it?

 

As far as I know it shouldn't be necessary to add the GSAP instance in the dependencies array of the use effect, that only tells React in which cases that code should run, nothing else. If you check my sample I don't add the instance to the useEffect call:

useEffect(() => {
  if (rotationTween) rotationTween.reversed(!rotate);
}, [rotate]);

Also another alternative that I would try is to tween the timescale of the animation, in order to create a smooth start/end for the animation, like this sample:

See the Pen OXXXzK by GreenSock (@GreenSock) on CodePen

There you can see that the code tweens the timescale property to 0 and 1 to create the smooth start/stop.

  • Like 2
Link to comment
Share on other sites

I dont really get how this whole GSAP instance inside useState should work when we initiate them inside a useEffect. How do you suggest the dynamic props are passed into the instance?

 

In this Pen I added the instances into react useState. It's not working because they are not added into the dependency array. When I do it also fails.

See the Pen YzwGgMw by paulborm (@paulborm) on CodePen

 

Quote

Also another alternative that I would try is to tween the timescale of the animation, in order to create a smooth start/end for the animation, like this sample:

Timescale does not work in this case because the wheel must stop at an asynchronus point.

Link to comment
Share on other sites

I tried a lot but anything will result in a rerender of the component which will kill the animation...

 

Is it possible to use the same timeline/tween at the same "play head" after a rerender happened?  I see no way to achieve this. Can't imagine I'm the only one with this problem. 

 

How would you handle this in a class component? I dont see a way to do it either.

 

thx

Link to comment
Share on other sites

Hi,

 

This works with a class component:

 

https://codesandbox.io/s/gsap-react-stop-wheel-h6fpv?file=/src/App.js

 

The approach is a bit different though. You'll have to find a correlation between the end angle and the actual landing spot of the wheel, or try to implement your own logic for that. Also I don't know what on earth is going on in Codesandbox but the same code is working as expected here:

 

https://gsap-stop-wheel.netlify.app/

 

Here works as expected too:

https://stackblitz.com/edit/gsap-react-stop-wheel?file=index.js

 

Happy Tweening!!!

  • Like 5
Link to comment
Share on other sites

This looks great at first glance!

 

I'll have a look to the whole stopping angle stuff. You used the timescale technice which is a bit tricky for stopping at a specific angle that's why I used different tweens in the first place.

 

Will come back soon to this thread.

 

Thank you!! :) 

 

 

 

Link to comment
Share on other sites

I made the stop position customizable and pass it into the wheel componenent. There I moved away from the infinite spinning with onRepeat and used onComplete to check each time if it's time to stop. It kinda works and the correct stop position is guaranteed. 

What is your oppinion to this approach?

 

https://stackblitz.com/edit/gsap-react-stop-wheel-vyxgea

 

Now all the tweens are inside componentDidMount() - is this the way to go? How would you implement a reset feature e.g. after the wheel stopped, one can play it again.

Link to comment
Share on other sites

Hi Paul, I am not across the whole thread but just looked at your demo and see that you are trying to manage the spinning state in both the App and Wheel components. Shouldn't you just have one spinning state?

  • Like 1
Link to comment
Share on other sites

Quote

Hi Paul, I am not across the whole thread but just looked at your demo and see that you are trying to manage the spinning state in both the App and Wheel components. Shouldn't you just have one spinning state?

Yes good point. But in this case it's really secondary and I implemented it just to disable some buttons while spinning

Link to comment
Share on other sites

To bring this thread to an end. I made your code @Rodrigo to work with functional components without killing the tween on prop caused re-renders..

 

https://stackblitz.com/edit/gsap-react-stop-wheel-cl3pqo

 

In order to use dynamically updating props in the spinning Tween, I used React.useRef to save those values. Refs dont trigger rerender and therefor the tween won't be killed and reinitialized or something. They can be used in the useEffect hook without it running again.

 

  React.useEffect(() => {
    if (typeof stopAt !== "number") return;
    stopAtRef.current = stopAt;
  }, [stopAt]);
onComplete: () => {
  console.log("onComplete", { stopAt, stopAtRef: stopAtRef.current });
  
  // The stop position can be checked on every tween iteration without useEffect running again
  if (loopIterationRef.current >= 2 && typeof stopAtRef.current === "number") {
    spinTweenRef.current.timeScale(0);

    gsap.to(wheelRef.current, {
      rotation: `+=${stopAtRef.current + 360}`,
      duration: (1.8 * (stopAtRef.current + 360)) / 360,
      onComplete: () => {
        setSpinning(false);

        if (onStopSpinning) {
          onStopSpinning();
        }
      },
    });
  } else {
    loopIterationRef.current++;
    spinTweenRef.current.play(0);
  }
};

 

I really appreciate your help. This forum and gsap are just great.

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×