Jump to content
GreenSock

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

Can't get reverse() to work in React

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

Hi.  I have the following component in React, a hamburger icon, which works just fine as written.  My issue is I'm trying to make the component more concise by using the reverse() method on my timeline, which I can't get to work as intended.  I've read through a few posts to try and remedy the issue, but it's not coming to me.

const NavHamburger = props => {
  const tl = new TimelineMax()
  // use useRef to detect first render
  const isFirstRender = useRef(true)
 
  useEffect(() => {
    // prevent hamburger from animating on first render
    if (isFirstRender.current === true) {
      isFirstRender.current = false
    } else {
      props.menuExpanded === true
        ? tl
            .to([".top"".bottom"]0.2, { y: "0%" }, 0)
            .to(".middle"0, { autoAlpha: 0 }, 0.2)
            .to(".top"0.2, { transform: "rotate(45deg)" }, 0.2)
            .to(".bottom"0.2, { transform: "rotate(-45deg)" }, 0.2)
        : tl
            .to([".top"".bottom"]0.2, { transform: "rotate(0)" }, 0)
            .to(".bottom"0.2, { transform: "rotate(0)" }, 0)
            .to(".middle"0, { autoAlpha: 1 }, 0.2)
            .to(".top"0.2, { y: "-400%" }, 0.2)
            .to(".bottom"0.2, { y: "400%" }, 0.2)
    }
  }, [props.menuExpanded])
 
  return (
    <Container
      className="nav-hamburger"
      onClick={() => props.setMenuExpanded(!props.menuExpanded)}
    >
      <Inner>
        {/* bars of hamburger */}
        <Top className={"top" + props.menuState}></Top>
        <Middle className={"middle" + props.menuState}></Middle>
        <Bottom className={"bottom" + props.menuState}></Bottom>
      </Inner>
    </Container>
  )
}
 
export default NavHamburger

 

I'm specifically trying to replace the following section of code with tl.reverse().  Any direction would be greatly appreciated.  I have a feeling that it has to to with how I'm declaring my timeline, but can't figure out the correct way to go about it.  Thanks.

 tl
            .to([".top"".bottom"]0.2, { transform: "rotate(0)" }, 0)
            .to(".bottom"0.2, { transform: "rotate(0)" }, 0)
            .to(".middle"0, { autoAlpha: 1 }, 0.2)
            .to(".top"0.2, { y: "-400%" }, 0.2)
            .to(".bottom"0.2, { y: "400%" }, 0.2)

 

Link to comment
Share on other sites

Hello and welcome to the forums ZSW. 

 

Generally speaking, you should create the timeline when your component is initialized and have it be paused. Then, when you need to, you play it forwards (tl.play()) or reversed (tl.reverse()). You could even do both conditionally in one line by using tl.reversed() ? tl.play() : tl.reverse();

 

Generally speaking it's a good idea to provide a demo for debugging help like this. It makes helping you easier :) You could use something like StackBlitz or CodePen to create a minimal demo.

  • Like 5
Link to comment
Share on other sites

@ZachSaucier is right, is not a good idea to populate timelines inside a useEffect hook. The main issue is that the timeline resides outside the scope of the hook and therefore, everytime the hook is executed all those instance are added to the end of the timeline, so your timeline has more and more instances. On top of that when those new instances are created the starting values might not be the ones you're looking for, so you might see some odd behaviour in your animations.

 

Create the animation using an useEffect hook with an emtpy array (this will run only in the initial render) and another one to toggle the animation. Also for toggle instances I like the pattern of creating the timeline paused and add a reverse() method at the end, like this:

 

const tl = new TimelineLite({ paused: true });

tl
  .to()
  .to()
  .reverse();

// then in the toggle method
const toggleTimeline = function () {
  tl.reversed(!tl.reversed());
}

Here is a reduced sample on how to toggle a GSAP instance using Hooks, the only difference is that a TweenLite instance is being created instead of a timeline, but making it work with a timeline shouldn't be too hard though:

 

https://stackblitz.com/edit/gsap-react-hooks-toggle

 

Happy Tweening!!!

  • Like 5
Link to comment
Share on other sites

Alright.  I got it to work.  I was able to do it initializing the timeline with useRef() and useState().  You can see my CodePens below.  Is there a preference with useRef() vs useState()?

 

Another question, maybe it's just a bug in CodePen... In both examples, if you click the icon enough times, the middle layer will disappear and not come back for that click cycle, but will come back on subsequent clicks.  It seems to be totally random, as there is no number of clicks that consistently triggers it.  What I do notice though, is that it happens more often the longer you keep clicking (like 50+ times).  Any thoughts?

 

Hamburger - initialize w/ useRef()

See the Pen vYBVawj by ZeeEssDoubleU (@ZeeEssDoubleU) on CodePen

Hamburger - initialize w/ useState()

See the Pen YzKJjmL by ZeeEssDoubleU (@ZeeEssDoubleU) on CodePen

Link to comment
Share on other sites

Yeah sometimes zero duration instances can be a bit painful in timelines. Using a very short .to() instance (0.01 seconds) gives the same effect and doesn't have the bug you describe.

 

Also you don't need to pass transform to the instances, GSAP has it's own simplified syntax for transforms. You can use this instead:

 

React.useEffect(() => {
  tl
    .to([top.current, bottom.current], 0.2, { y: "0%" }, 0)
    .to(middle.current, 0.01, { autoAlpha: 0 }, 0.2)
    .to(top.current, 0.2, { rotation:45 }, 0.2)
    .to(bottom.current, 0.2, { rotation: -45 }, 0.2);
}, []);

See?, much simpler and less error prone ;)

 

Happy Tweening!!!

  • Like 2
Link to comment
Share on other sites

You have a spelling error here, which makes hovering difficult because the Inner element has no height.

 

hamVars.layerSpacing = 3 * hamVars.layherHeight; // Bad
hamVars.layerSpacing = 3 * hamVars.layerHeight; // Good

 

10 hours ago, ZeeEssDoubleU said:

Is there a preference with useRef() vs useState()?

 

I found this little line of code in your demo to be quite interesting. It logs out timeline every time you click on your component, which confused me because I assumed it would only run once. That was an Aha! moment for me.

 

// DEBUG
console.log("timeline:", tl);

 

That means React is calling your NavHamburger function on every render. So this will create a new timeline on every render, which will cause problems if you're watching something with useEffect.

 

const NavHamburger = props => {
  const tl = new TimelineMax(); // Creates a new timeline on every render
}

 

Initializing useRef or useState with a timeline will also create a new timeline on every render, but it saves the first timeline, so any newly created timelines after that will be discarded. Creating a bunch of timelines that will never be used is wasteful and could cause performance problems.

 

const tl = useRef(new TimelineMax()); // Creates a new timeline on every render

const [tl, setTl] = useState(new TimelineMax()); // Creates a new timeline on every render

 

So after going through all of this, I think it's better to use useRef when creating animations. useRef is just like a variable, and won't trigger a re-render when you change it, but useState will.

 

const tl = useRef();

useEffect(() => {
  tl.current = new TimelineMax({ paused: true })
    .to([top.current, bottom.current], 0.2, { y: "0%" }, 0)
    .to(middle.current, 0.01, { autoAlpha: 0 }, 0.2)
    .to(top.current, 0.2, { rotation:45 }, 0.2)
    .to(bottom.current, 0.2, { rotation: -45 }, 0.2);
}, []);

 

Here's @Rodrigo's demo using that approach.

https://stackblitz.com/edit/gsap-react-hooks-toggle-y2xpnu

 

And here's a good article that goes over useRef and useEffect.

https://leewarrick.com/blog/react-use-effect-explained/

 

 

  • Like 4
Link to comment
Share on other sites

Thanks for pointing out the typo.  Ill get that fixed.

 

I’ll use useRef() going forward and move the timeline initialization out of the useRef() call.  It definitely sounds like the better option.

 

As for the component re-rendering every click.  NavHamburger actually has ‘menuExpanded’ state that is toggled on and off with each click.  I included it for demonstration in this CodePen, because I'm currently using NavHamburger in an app where its parent’s (a nav bar) ‘menuExpanded’ state is passed to it through props.  So unfortunately, the NavHamburger will re-render due to the 'menuExpanded' state change.

  • Like 2
Link to comment
Share on other sites

3 hours ago, ZeeEssDoubleU said:

As for the component re-rendering every click. The hamburger actually has ‘menuExpanded’ state that is toggled on and off with each click.  I put it for demonstration in this CodePen, because Im currently using the hamburger in an app where its parent’s (nav bar) ‘menuExpanded’ state is passed to it through props.  So unfortunately, the navBar will re-render due to that state change.

 

Right. I get why it re-renders, but I'm a little new to Hooks, so I was expecting it to behave more like a class. With a class, it would just call the render method on a re-render. Internally, it would look something like this.

 

class NavHamburger extends Component {
  render() {
    return ...
  }
}

// initial
const navHamburger = new NavHamburger(props);
navHamburger.render();

// re-render
navHamburger.render();

 

With Hooks, it's more like this.

 

const NavHamburger = props => {
  ...
};

// initial
NavHamburger(props);

// re-render
NavHamburger(props);

 

But it makes sense now that I think about it, and your console.log statement helped me realize what's really going on, so a big thanks for that! ?

 

 

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