Jump to content
Search Community

Animation runs every time I click buttons that starts animations

Devotee007 test
Moderator Tag

Go to solution Solved by Devotee007,

Recommended Posts

I have a React component for a top menu. I have animated the menu with CSS (Tailwind), but I want, of course, use GSAP instead. I have just started and this is the first time I use React, so a bit of a newbie. Below is the code that I have for the GSAP animation. The thing that happens in then I click on the buttons that starts CSS-animations the GSAP animation below fires as well. The page doesn't reload or anything. Is there something in the below code that I have forgotten to use? 


 

export const TopMenu: React.FC<TopMenuProps> = ({
  primaryNavLinks,
  secondaryNavLinks,
  languages
}) => {
  // GSAP CODE
  const component = useRef(null);

  useEffect(() => {
    let ctx = gsap.context(() => {
      console.log("GSAP");
      gsap.from(".box", { opacity: 0, y: -90, ease: "power3", duration: 2 });
    }, component);
  });
  //END GSAP CODE

 

Link to comment
Share on other sites

It's pretty tough to troubleshoot without a minimal demo - the issue could be caused by CSS, markup, a third party library, your browser, an external script that's totally unrelated to GSAP, etc. Would you please provide a very simple CodePen or CodeSandbox that demonstrates the issue? 

 

Please don't include your whole project. Just some colored <div> elements and the GSAP code is best (avoid frameworks if possible). See if you can recreate the issue with as few dependancies as possible. If not, incrementally add code bit by bit until it breaks. Usually people solve their own issues during this process! If not, then at least we have a reduced test case which greatly increases your chances of getting a relevant answer.

 

Here's a starter CodePen that loads all the plugins. Just click "fork" at the bottom right and make your minimal demo

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

 

If you're using something like React/Next/Vue/Nuxt or some other framework, you may find StackBlitz easier to use. We have a series of collections with different templates for you to get started on these different frameworks: React/Next/Vue/Nuxt.

 

Here is a starter template using GSAP and React:

https://stackblitz.com/edit/gsap-react-basic-f48716

 

Once we see an isolated demo, we'll do our best to jump in and help with your GSAP-specific questions. 

Link to comment
Share on other sites

I got the below code to work so that the animation just play on button click, but it doesn't reverse, it plays the animation from the start every time I click the button.  So even if handleMenuClick is true or false it plays the timeline forward.

 

 
const [isBoxVisible, setIsBoxVisible] = useState(false);
  const [menuClicked, setMenuClicked] = useState(false);

  const tl = gsap.timeline({ paused: true, reversed: true });
  tl.set(".box", { y: 0 });
  tl.to(".box", { y: 90, ease: "power3", duration: 2 });

  function handleMenuClicked() {
    setMenuClicked(prevMenuClicked => {
      console.log("before", prevMenuClicked);
      const newMenuClicked = !prevMenuClicked;
      console.log("after", newMenuClicked);
      return newMenuClicked;
    });
  }

  useEffect(() => {
    setIsBoxVisible(true);
  }, []);

  useEffect(() => {
    console.log("IF: " + menuClicked);
    tl.progress() === 0 ? tl.play() : tl.reverse();
  }, [menuClicked, tl]);

 

Link to comment
Share on other sites

It's very difficult to troubleshoot without a minimal demo, but it looks to me like your problem is that you're creating your timeline outside of any useLayoutEffect()/useEffect() functions, thus it gets re-created on EVERY render. I'm not a React guy, so I could be wrong. 

 

Maybe you meant to do something more like this?: 

const [isBoxVisible, setIsBoxVisible] = useState(false);
const [menuClicked, setMenuClicked] = useState(false);
const tl = useRef();

function handleMenuClicked() {
	setMenuClicked(prevMenuClicked => {
		console.log("before", prevMenuClicked);
		const newMenuClicked = !prevMenuClicked;
		console.log("after", newMenuClicked);
		return newMenuClicked;
	});
}

useLayoutEffect(() => {
	let ctx = gsap.context(() => {
		tl.current = gsap.fromTo(".box", { 
			y: 0 
		}, { 
			y: 90, 
			reversed: true,
			ease: "power3", 
			duration: 2 
		});
	});
	setIsBoxVisible(true);
	return () => ctx.revert();
}, []);

useLayoutEffect(() => {
	console.log("IF: " + menuClicked);
	tl.current.progress() === 0 ? tl.current.play() : tl.current.reverse();
}, [menuClicked]);

Remember, gsap.context() is your best friend in React. See this article: 

 

If you're still having trouble, please make sure you provide a minimal demo. Here's a starter you can fork: https://stackblitz.com/edit/gsap-react-basic-f48716

Link to comment
Share on other sites

I have tried to set up a bit of my code stackblitz. I don't know if that's enough code? I have tried to use current in different ways but I get the error "Property 'current' does not exist on type 'void'.ts(2339)" so I have reverted back to the code seen  on the link. Can it be that current won't work when I have it inside this:

export const TopMenu: React.FC<TopMenuProps> = ({
primaryNavLinks,
secondaryNavLinks,
languages
}) => {--};

 
Here's the link to stackblitz: https://stackblitz.com/edit/gsap-react-basic-f48716-8mtrkg?file=src%2FApp.js,src%2Fgsap-react.tsx

 



 

Link to comment
Share on other sites

I have looked at this code https://stackblitz.com/edit/gsap-react-basic-f48716?file=src%2FApp.js and copied:
 

const container = useRef();
  const tl = useRef();

  const toggleTimeline = () => {
    tl.current.reversed(!tl.current.reversed());
  };

  useLayoutEffect(() => {
    const ctx = gsap.context((self) => {
      const boxes = self.selector('.box');
      tl.current = gsap
        .timeline()
        .to(boxes[0], { x: 120, rotation: 360 })
        .to(boxes[1], { x: -120, rotation: -360 }, '<')
        .to(boxes[2], { y: -166 })
        .reverse();
    }, container); // <- Scope!
    return () => ctx.revert(); // <- Cleanup!
  }, []);


and put it inside of of this:

export const TopMenu: React.FC<TopMenuProps> = ({
  primaryNavLinks,
  secondaryNavLinks,
  languages
}) => {

...
};


Inside of

return (..);


I have put these two on a div and a button.
 

ref={container} 

onClick={toggleTimeline}


Doing this I get error on tl.current ('tl.current' is possibly 'undefined'.ts(18048)) and self.selector (

Cannot invoke an object which is possibly 'undefined'.ts(2722)
'self.selector' is possibly 'undefined'.ts(18048)

)
Is this because the code is inside of

export const TopMenu: React.FC<TopMenuProps> = ({
  primaryNavLinks,
  secondaryNavLinks,
  languages
}) => {...};

Or what else have I missed? 

I just want a timline that can play forward on button click and reverse itself on next click on same button. The timeline I want to use is this one, 

See the Pen JjBgQrO?editors=0010 by Devotee007 (@Devotee007) on CodePen

Link to comment
Share on other sites

I solved it with this code, taken from: https://codesandbox.io/s/gsap-hamburger-toggle-menu-u07i6?file=/src/App.js:1045-1073,
 

const [tlMenu] = useState(gsap.timeline({ paused: true }));

  useEffect(() => {
    tlMenu.to(
      ".js-mobile-overlay",
      {
        // yPercent: 170,
        y: "85vh",
        ease: "expo.inOut",
        duration: 1.1
      },
      0
    );

    tlMenu.to(
      ".js-mobile-menu",
      {
        y: 0,
        ease: "expo.inOut",
        duration: 0.9
      },
      0.1
    );

    tlMenu.to(
      ".js-pie-top",
      {
        strokeDasharray: "0,100",
        autoRound: false,
        ease: "expo.in",
        duration: 0.5
      },
      0.1
    );

    tlMenu.to(
      ".js-pie-bottom",
      {
        strokeDasharray: "0,100",
        strokeDashoffset: "-25",
        autoRound: false,
        ease: "expo.in",
        duration: 0.5
      },
      0.1
    );

    tlMenu.to(
      ".menu-toggle-titles",
      {
        y: -14,
        ease: "power4.inOut",
        duration: 0.4
      },
      0.5
    );

    tlMenu
      .to(
        ".js-pie-middle",
        {
          rotation: 90,
          y: 8,
          x: -4,
          transformOrigin: "center",
          ease: "back.out(1.4)",
          duration: 0.5
        },
        0.6
      )

      .reverse();
  }, []);

  const toggleMobileMenu = () => {
    tlMenu.reversed(!tlMenu.reversed());

    setTopMenuVisible(!activeMobileTopMenu);
    setActiveSubLevel(-1);
    setActiveSubSubLevel(-1);
  };

Still don't know why it works though. Shouldn't I need context to be able to use different classes and so on? 

Link to comment
Share on other sites

Glad you got it working, but I would suggest against using that code as it's not doing correct animation cleanup and it's using an old version of React (hence why it's working without cleanup, this is mainly important after React 18 due to strict mode)

If you do go ahead with it, you may run into issues.  Just a little FYI so you're aware. ☺️

Link to comment
Share on other sites

Hi,

 

As @Cassie mentions that is a very old example. Is better to use this as a reference:

https://stackblitz.com/edit/vitejs-vite-4jhqox

 

This is also another option leveraging all the options GSAP Context offers to achieve the same result:

function App() {
  const svgContainer = useRef();
  const ctx = useRef();

  const toggleMenuTimeline = () => {
    ctx.current.toggleMenu();
  };

  useLayoutEffect(() => {
    ctx.current = gsap.context((self) => {
      const menuTl = gsap
        .timeline({ paused: true })
        .to('#topBar', {
          duration: 0.2,
          x: 52,
          stroke: '#006600',
          rotation: 45,
        })
        .to('#middleBar', { duration: 0.2, alpha: 0 }, 0)
        .to(
          '#bottomBar',
          { duration: 0.2, x: 52, stroke: '#006600', rotation: -45 },
          0
        )
        .reverse();
      self.add('toggleMenu', () => {
        menuTl.reversed(!menuTl.reversed());
      });
    }, svgContainer);
    return ctx.current.revert();
  }, []);

  return (/*...*/);
}

 

Happy Tweening!

Link to comment
Share on other sites

7 minutes ago, Rodrigo said:

Hi,

 

As @Cassie mentions that is a very old example. Is better to use this as a reference:

https://stackblitz.com/edit/vitejs-vite-4jhqox

 

This is also another option leveraging all the options GSAP Context offers to achieve the same result:

function App() {
  const svgContainer = useRef();
  const ctx = useRef();

  const toggleMenuTimeline = () => {
    ctx.current.toggleMenu();
  };

  useLayoutEffect(() => {
    ctx.current = gsap.context((self) => {
      const menuTl = gsap
        .timeline({ paused: true })
        .to('#topBar', {
          duration: 0.2,
          x: 52,
          stroke: '#006600',
          rotation: 45,
        })
        .to('#middleBar', { duration: 0.2, alpha: 0 }, 0)
        .to(
          '#bottomBar',
          { duration: 0.2, x: 52, stroke: '#006600', rotation: -45 },
          0
        )
        .reverse();
      self.add('toggleMenu', () => {
        menuTl.reversed(!menuTl.reversed());
      });
    }, svgContainer);
    return ctx.current.revert();
  }, []);

  return (/*...*/);
}

 

Happy Tweening!

Thanks will try this. But I had problems with current and useRef  and to have it in it's own function App(){}. before I got it to work. 

I have the timeline directly inside of the code below, is that why it doesn't work with a function, does it have to be it's own component? 
 

export const TopMenu: React.FC<TopMenuProps> = ({
primaryNavLinks,
secondaryNavLinks,
languages
}) => {
export const TopMenu: React.FC<TopMenuProps> = ({
  primaryNavLinks,
  secondaryNavLinks,
  languages
}) => { ... };


 

Link to comment
Share on other sites

58 minutes ago, Cassie said:

Glad you got it working, but I would suggest against using that code as it's not doing correct animation cleanup and it's using an old version of React (hence why it's working without cleanup, this is mainly important after React 18 due to strict mode)

If you do go ahead with it, you may run into issues.  Just a little FYI so you're aware. ☺️

Thanks for the heads up! Will see if I get it to work with the code example below. 

Link to comment
Share on other sites

Inside  toggleMobileMenu I get error that 

menuTl.current' is possibly 'undefined'
const toggleMobileMenu = () => {
    menuTl.current.reversed(!menuTl.current.reversed());
  };


But inside the useLayOutEffect I don't get any warnings. I tried to add this to the const for menuTl
 

 const menuTl = useRef<GSAPAnimation>();

But it didn't do any difference. 

EDIT:

I managed to remove the errors with this code:

 

const btn = useRef<HTMLButtonElement>(null);
 const menuTl = useRef<GSAPTimeline>(gsap.timeline());

  const toggleMobileMenu = () => {
    console.log("test");
    if (menuTl.current !== null) {
      console.log("test2");
      // menuTl.current.reversed(!menuTl.current.reversed());
      menuTl.current.reversed(!menuTl.current.reversed());
    }
  };

  useLayoutEffect(() => {
    const ctx = gsap.context(self => {
      menuTl.current = gsap.timeline({ paused: true });
      menuTl.current.to(
        ".js-mobile-overlay",
        {
          // yPercent: 170,
          y: "85vh",
          ease: "expo.inOut",
          duration: 1.1
        },
        0
      );

      menuTl.current
        .to(
          ".js-pie-top",
          {
            strokeDasharray: "0,100",
            autoRound: false,
            ease: "expo.in",
            duration: 0.5
          },
          0.1
        )
        .reverse();
    }, btn);
    return () => ctx.revert();
  }, []);

But nothing happen when I click the button. I get this error below in the console. But I got the class mentioned in the code.

 

react_devtools_backend.js:2655 GSAP target .js-mobile-overlay not found. https://greensock.com 
    at TopMenu (http://localhost:6006/components-stories-TopMenu-stories.iframe.bundle.js:370:30)
    at unboundStoryFn (http://localhost:6006/vendors-node_modules_storybook_addon-actions_preview_js-node_modules_storybook_addon-backgrou-313dcc.iframe.bundle.js:16918:12)
    at ErrorBoundary (http://localhost:6006/vendors-node_modules_storybook_addon-actions_preview_js-node_modules_storybook_addon-backgrou-313dcc.iframe.bundle.js:14550:5)
    at WithCallback (http://localhost:6006/vendors-node_modules_storybook_addon-actions_preview_js-node_modules_storybook_addon-backgrou-313dcc.iframe.bundle.js:14430:23)

 

Link to comment
Share on other sites

  • Solution

I got it to work now! :) Thanks @Cassie and @Rodrigo for the input and help!

Working code:
 

  const mobileMenu = useRef<HTMLDivElement>(null);
  const menuTl = useRef<GSAPTimeline>(gsap.timeline());

  const toggleMobileMenu = () => {
    if (menuTl.current !== null) {
      menuTl.current.reversed(!menuTl.current.reversed());

      setTopMenuVisible(!activeMobileTopMenu);
      setActiveSubLevel(-1);
      setActiveSubSubLevel(-1);
    }
  };

  useLayoutEffect(() => {
    const ctx = gsap.context(self => {
      menuTl.current = gsap.timeline({ paused: true });
      menuTl.current.to(
        ".js-mobile-overlay",
        {
          // yPercent: 170,
          y: "85vh",
          ease: "expo.inOut",
          duration: 1.1
        },
        0
      );

      menuTl.current.to(
        ".js-mobile-menu",
        {
          y: 0,
          ease: "expo.inOut",
          duration: 0.9
        },
        0.1
      );

      menuTl.current.to(
        ".js-pie-top",
        {
          strokeDasharray: "0,100",
          autoRound: false,
          ease: "expo.in",
          duration: 0.5
        },
        0.1
      );

      menuTl.current.to(
        ".js-pie-bottom",
        {
          strokeDasharray: "0,100",
          strokeDashoffset: "-25",
          autoRound: false,
          ease: "expo.in",
          duration: 0.5
        },
        0.1
      );

      menuTl.current.to(
        ".menu-toggle-titles",
        {
          y: -14,
          ease: "power4.inOut",
          duration: 0.4
        },
        0.5
      );

      menuTl.current
        .to(
          ".js-pie-middle",
          {
            rotation: 90,
            y: 8,
            x: -4,
            transformOrigin: "center",
            ease: "back.out(1.4)",
            duration: 0.5
          },
          0.6
        )
        .reverse();
    }, mobileMenu);
    return () => ctx.revert();
  }, []);

 

  • Like 2
Link to comment
Share on other sites

  • 4 months later...
On 4/3/2023 at 2:07 PM, Cassie said:

Hi there, have you read through this section in our React article? It seems like it's exactly what you're after? Fingers crossed this helps!

https://greensock.com/react-basics#timelines

 

 

Demo
 

 

Hi Cassie. Not sure how often it would impact gsap, but I think there's a slim chance`setReversed(!reversed)` can use a stale value here? Think the function argument `setReversed((currValue) => !currValue)` should be used if the new state is based on the old state :)

Link to comment
Share on other sites

Hi,

 

Indeed is better to use a function argument in the set callback of a useState hook. But be aware that the most common approach is to use either a useEffect or useCallback for those cases and unless you update the the callback, the value in the scope of the useEffect will always be the initial one.

 

Check this example:

https://stackblitz.com/edit/vitejs-vite-nkwsxv?file=src%2FApp.jsx&terminal=dev

 

While the count value is updated in the DOM, inside the useEffect hook is always 0. That has nothing to do with stale values, that's just Javascript scope and context execution. Just a warning, because I've seen that problem in a few examples here in the forums and users tend to think that GSAP is somehow related to it.

 

Happy Tweening!

Link to comment
Share on other sites

11 hours ago, Rodrigo said:

Hi,

 

Indeed is better to use a function argument in the set callback of a useState hook. But be aware that the most common approach is to use either a useEffect or useCallback for those cases and unless you update the the callback, the value in the scope of the useEffect will always be the initial one.

 

Check this example:

https://stackblitz.com/edit/vitejs-vite-nkwsxv?file=src%2FApp.jsx&terminal=dev

 

While the count value is updated in the DOM, inside the useEffect hook is always 0. That has nothing to do with stale values, that's just Javascript scope and context execution. Just a warning, because I've seen that problem in a few examples here in the forums and users tend to think that GSAP is somehow related to it.

 

Happy Tweening!

Hi Rodrigo

Yeah I'm very new to React so hadn't really thought about/been tripped up by using state in a useEffect, but that's definitely the sort of rake I'd have spent hours walking in to, so thanks!

So it's because the useEffect runs once with the value count initially has, whereas setCount's function argument is run with the actual state each time it's called?

Link to comment
Share on other sites

Hi,

 

Yeah, pretty much this has to do with the way React works internally running the useEffect/LayoutEffect hooks. Honestly I never went through react's source in order to see how this works, but in these cases I prefer to either use an external method (a function created outside the effect hook scope) and if necessary wrap that particular method in an useCallback hook if the component has a lot of state properties that could trigger re-creating the method over and over:

https://react.dev/reference/react/useCallback

 

Happy Tweening!

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...