Jump to content
Search Community

Gsap reverse not working in React

omarel test
Moderator Tag

Recommended Posts

I'm simply trying to run a timeline on click and reverse on click again. I'm sending the click state as a prop to the expanded menu and then if its true it should run the timeline, if not it should reverse.  What am I doing wrong. Why won't the timeline simply reverse with tl.reverse()

 

https://stackblitz.com/edit/nextjs-3ejdzo (update: Working Demo now according to content in the thread below)

The reverse is in this file:

https://stackblitz.com/edit/nextjs-3ejdzo?file=components%2FExpandedMenu.js

 

I've seen this same question asked few times and Ive reviewed the previous responses to my question but they all have somewhat different implementations.

 

Thank you.

Link to comment
Share on other sites

Hi @omarel. It's just a logic issue in your code - every time you click, it's creating a NEW timeline that's animating it from the CURRENT state to top: 0, opacity: 1. So think about what happens the second time you click - it's already at those values so there's no more animation. It doesn't matter if you reverse() because your animation is going from top: 0, opacity: 1 to exactly those same values. See the issue? 

 

I think it'd be much simpler to just replace all that timeline code with: 

gsap.to(menu.current, {
  top: toggle ? "0px" : "100vh",
  opacity: toggle ? 1 : 0,
  duration: 0.7,
});

That way it's always going to the correct values. 

 

Does that clear things up? 

Link to comment
Share on other sites

Hmm, I tried that but it didn't work. I'm not fully clear as my understanding is that the timeline would be reversed according to the CSS values that the elements started with which is in the scss file. top:-100vh

 

But if the issue is not within this code block, but that every time you click it's creating a new timeline, I suppose my first go to is to stop that from happening? If so, I suppose I can rethink the logic if you can offer any suggestions.

 

ExpandedMenu.js

  //state passes as prop from click on parent component
	const toggle = props.toggle

  useEffect(() => {

    const tl = gsap.timeline({
      paused: true,
    });
    tl.to(
      menu.current,
      {
        top: 0,
        duration: 0.7,
      },
      '+=0'
    );
    toggle === true ? tl.play() : tl.reverse();
    console.log(toggle);
  }, [toggle]);

 

ExpandedMenu.module.scss

.expandedmenu {
  position: fixed;
  top: -100vh; //css starting style
  left: 0;
  width: 100%;
  height: 100vh;
  background-color: mixin.$color-darkblue;
  z-index: 9;
}

 

Link to comment
Share on other sites

I found another example and it seems to be working now with some adjustments. There were 3 adjustments. 

 

It works! but is this ok in terms of not triggering multiple timelines on top of each other?

  • Move the timeline out of useEffect
  • use useMemo() - still not fully sure why
  • Creating 2 UseEffects, mount timeline on load and another that mounts play() function with the toggle state changes

 

https://stackblitz.com/edit/nextjs-3ejdzo?file=components%2FExpandedMenu.js

 

import { useEffect, useRef, useState, useMemo } from 'react';
import styles from './ExpandedMenu.module.scss';
import { gsap } from 'gsap';

export default function ExpandedMenu(props) {
  const menu = useRef();
  const toggle = props.toggle;

  const tl = useMemo(() => gsap.timeline({ paused: true }), []); //timeline with useMemo

  useEffect(() => {
    tl.to(
      menu.current,
      {
        top: 0,
        duration: 0.7,
      },
      '+=0'
    );
  }, []);  //useEffect 1

  useEffect(() => {\
    
    if (toggle) {
      tl.play();
    } else {
      tl.reverse();
    }
    console.log(toggle);
    
  }, [toggle]); //useEffect 2

  return (
    <>
      <section className={`${styles.expandedmenu}`} ref={menu}></section>
    </>
  );
}

 

Link to comment
Share on other sites

and I discovered this works like above as well - without using useMemo - while keeping the timeline inside there first useEffect and using useRef on the timeline. Leaving for reference:

 

  const menu = useRef()
  const tl = useRef()
  const toggle = props.toggle

  useEffect(() => {

    tl.current = gsap.timeline({ paused: true });
    tl.current.to(menu.current, { 
      top: 0,
      duration:0.7,
      }, "+=0")

  },[])

  useEffect(() => {   

    toggle ? tl.current.play() : tl.current.reverse()
    console.log(toggle)

  }, [toggle]);

 

Link to comment
Share on other sites

11 hours ago, omarel said:

Hmm, I tried that but it didn't work

Are you sure? It worked great for me when I dropped that code into your demo. I'm pretty sure it's the simplest, cleanest solution.

 

11 hours ago, omarel said:

my understanding is that the timeline would be reversed according to the CSS values that the elements started with which is in the scss file. top:-100vh

Yes, if you simply reversed that initial timeline, sure. But you're not doing that - you keep re-creating the timeline every time there's a toggle. So let's walk through this...

 

The first time the timeline gets created, you told it to go to top: 0. So the first time it renders, that tween says "okay what is the CURRENT value - that should serve as the 'start' and I'm going to animate that to top: 0". So it grabs the current value which happens to be 100vh at that point. Great! And it animates to 0. Perfect. 

 

Now you click again...and it says "okay, I'm gonna animate from the current value to top: 0...so let me grab the current value which is...0." DOH! See the problem? This new timeline has a start of 0 and an end of 0. So if you reverse() it, that doesn't help anything. The problem is the start/end values match! It's not a problem with GSAP - it's a logic issue in your code. 

 

That very first timeline that had the "start" value of 100vh could indeed be reversed and it'd go back to that. But you're not reversing that timeline after the first click - you keep creating a new timeline and trying to reverse() those but they have matching start/end values. 

 

There are many ways to solve that. You could explicitly set the start/end values by using a .fromTo() instead of a .to(). You could only create that one timeline and then just play()/reverse() that one (instead of recreating every time). The only problem I see with that is you're working with React and I suppose there's a chance that React would create an entirely different element (.current), so the one that the timeline is animating may not end up being the one on the screen (due to React swapping stuff out). 

 

By the way, there's no need to even create a timeline if you're only going to have one tween in it. I mean that's fine, it won't break anything. I'm just saying it's a tad wasteful. And you don't need to have "+=0" as the position parameter. That's doing nothing. The default position is at the end anyway. 

Link to comment
Share on other sites

On 2/27/2022 at 12:27 PM, omarel said:

and I discovered this works like above as well - without using useMemo - while keeping the timeline inside there first useEffect and using useRef on the timeline. Leaving for reference:

 

We recommend creating your timelines inside the effect. Please check out our React Guides.

 

 

  • Like 1
Link to comment
Share on other sites

This is great. My second example above pushing the timeline back into one of the use effects and creating a second use effect hook to deal with the toggle works and looks in line with the guide but I will definitely review this!!

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