Jump to content
Search Community

React + useState + forEach. Restarts the animation on `state` update

Pavel Laptev test
Moderator Tag

Go to solution Solved by SteveS,

Recommended Posts

Hi, gsappers! I have a question regarding `useState` and `forEach` method.

 

The issue: I want to switch the animation to the next step depending on the `step` state.

But if I change the state, the animation resets, instead of going seamlessly from the step one to step two and so on.

What could be the reason behind it? Should I create the timeline as a `ref` for each element I want to animate? 

 

Here is the demo — https://codesandbox.io/s/optimistic-sammet-6k76cn?file=/src/App.tsx

 

Code:

import "./styles.css";
import React from "react";

import gsap from "gsap";

const App = () => {
  const flowerCirclesRef = React.useRef<any>([]);
  const [step, setStep] = React.useState(0);

  const FlowerCircles = () => {
    return (
      <>
        {Array.from(Array(6).keys()).map((i) => {
          return (
            <div
              key={i}
              className="flower"
              ref={(el) => (flowerCirclesRef.current[i] = el)}
            />
          );
        })}
      </>
    );
  };

  React.useEffect(() => {
    flowerCirclesRef.current.forEach((el: any, i: number) => {
      if (step === 1) {
        gsap.fromTo(
          el,
          {
            transformOrigin: "left center",
            opacity: 0,
            scale: 0.1,
            borderWidth: 3,
            left: "50px"
          },
          {
            transformOrigin: "left center",
            rotate: 180 * (i / 3),
            scale: 1,
            opacity: 1,
            borderWidth: 1,
            duration: 1.3,
            delay: i * 0.1,
            ease: "elastic.out(0.5,0.9)"
          }
        );
      }

      if (step === 2) {
        gsap.to(el, {
          rotate: 100 * (i / 3),
          x: i * 100,
          scale: 2,
          opacity: 1,
          borderWidth: 1,
          duration: 1.3,
          delay: i * 0.1,
          ease: "elastic.out(0.5,0.9)"
        });
      }
    });
  }, [step]);

  return (
    <section className={"wrapper"}>
      <button onClick={() => setStep(step + 1)}>Next step</button>
      <div className="flowerwrap">
        <FlowerCircles />
      </div>
    </section>
  );
};

export default App;

 

 

Thank you!

Link to comment
Share on other sites

Every time the step state changes, your component will re-render and put the FlowerCircles into their initial, untweened position.

A few things:

  1. When working with GSAP and react, disable strict mode
  2. Instead of Array.from(...) you can do [...Array(10)] to get an empty array of length 10
  3. Any tween you create in an effect, you have to kill as part of that effect's cleanup

With respect to what you should do, I believe you have the right idea with create a timeline ref. In the timeline, put onComplete: () => tlRef.current.pause() so that the timeline pauses when each ends, then on click simply play the timeline.

I'm not sure if this is the absolute best way, but it should work.

  • Like 3
Link to comment
Share on other sites

@SteveS seems like I'm doing something wrong here https://codesandbox.io/s/mutable-waterfall-5y4l8m?file=/src/App.tsx:1706-1724

 

Also found this example, but it doesn't work with an array of timelines 🤔

 

 

import "./styles.css";
import React from "react";

import gsap from "gsap";

const App = () => {
  const flowerCirclesRef = React.useRef<any>([]);
  const flowerTimelinesRef = React.useRef<any>(
    [...Array(10)].map(() => gsap.timeline({ paused: true }))
  );
  const [step, setStep] = React.useState(0);

  const FlowerCircles = () => {
    return (
      <>
        {Array.from(Array(6).keys()).map((i) => {
          return (
            <div
              key={i}
              className="flower"
              ref={(el) => (flowerCirclesRef.current[i] = el)}
            />
          );
        })}
      </>
    );
  };

  React.useEffect(() => {
    if (flowerCirclesRef.current.length === 0) return;

    flowerCirclesRef.current.forEach((el: any, i: any) => {
      const timeline = flowerTimelinesRef.current[i];
      timeline
        .from(el, {
          transformOrigin: "left center",
          opacity: 0,
          scale: 0.1,
          borderWidth: 3,
          left: "50px"
        })
        .to(el, {
          transformOrigin: "left center",
          rotate: 180 * (i / 3),
          scale: 1,
          opacity: 1,
          borderWidth: 1,
          duration: 1.3,
          delay: i * 0.1,
          ease: "elastic.out(0.5,0.9)",
          onComplete: () => {
            flowerTimelinesRef.current[i].pause();
          }
        })
        .to(el, {
          rotate: 60 * (i / 3),
          x: 20 * i
        });
    });

    return () => {
      flowerTimelinesRef.current.forEach((el: any) => {
        el.kill();
      });
    };
  }, []);

  React.useEffect(() => {
    if (step === 1) {
      console.log("flowerTimelinesRef.current", flowerTimelinesRef.current);
      flowerTimelinesRef.current.forEach((timeline: any) => timeline.play());
    }
  }, [step]);

  return (
    <section className={"wrapper"}>
      <button onClick={() => setStep(step + 1)}>Next step</button>
      <div className="flowerwrap">
        <FlowerCircles />
      </div>
    </section>
  );
};

export default App;

 

Link to comment
Share on other sites

  • Solution

Again, relying on state for this, at least in the way you have constructed it, is not straightforward. Instead, control the timeline. Also, I'm pretty sure you are never supposed to declare components inside each other, such as you have with <FlowerCircles />.

Here is a CSB that works using the method I outlined:

https://codesandbox.io/s/dank-tree-9mq340?file=/src/App.tsx
 

Note: I'm not properly cleaning up the tweens or the timeline. To do so add them to an array and loop through the array in the cleanup to kill them.

  • Like 3
Link to comment
Share on other sites

3 minutes ago, SteveS said:

Again, relying on state for this, at least in the way you have constructed it, is not straightforward. Instead, control the timeline. Also, I'm pretty sure you are never supposed to declare components inside each other, such as you have with <FlowerCircles />.

Here is a CSB that works using the method I outlined:

https://codesandbox.io/s/dank-tree-9mq340?file=/src/App.tsx
 

Note: I'm not properly cleaning up the tweens or the timeline. To do so add them to an array and loop through the array in the cleanup to kill them.

Thank you, @SteveS wouldn't figure it out without you

  • Like 2
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.
×
×
  • Create New...