Jump to content
Search Community

GSAP reverse with React

Aquasar test
Moderator Tag

Recommended Posts

I have the following code and am trying to create a menu icon that closes and open. I am having issues getting the animation to run in reverse using React.

 

Here is my code snapshot,

  const [reversesetReverse= useState(true);
 
  function moveBurger() {
    const tl = new gsap.timeline({ pausedtruereversedreverse });
    tl.to('.burger_rect1'0.2, {
      rotation45,
      transformOrigin'50% 50%',
    })
      .to('.burger_rect2'0.2, { scaleX0 })
      .to('.burger_rect3'0.2, {
        rotation-45,
        transformOrigin'50% 50%',
      });
 
    if (reverse === true) {
      console.log('play it');
      tl.play();
    } else {
      console.log('reverse play it');
      tl.reverse();
    }
 
    setReverse(prev => !prev);
  }

 

I did a console.log and looks like it runs the animation fwd and it does not run the animation backward, but it logs 'reverse play it'

 

Any suggestions ?

 

 

Full component here

 

import React, { useState } from 'react';
 
import styled from '@emotion/styled';
 
import { gsap } from 'gsap';
 
const SVG = styled.svg`
  & rect {
    fill: #fff;
  }
  &:hover {
    & .line-one {
      stroke: red;
    }
  }
`;
 
const BurgerSVG2 = () => {
  // main mobile function, gets called on hover
 
  const [reversesetReverse= useState(true);
 
  function moveBurger() {
    const tl = new gsap.timeline({ pausedtruereversedreverse });
    tl.to('.burger_rect1'0.2, {
      rotation45,
      transformOrigin'50% 50%',
    })
      .to('.burger_rect2'0.2, { scaleX0 })
      .to('.burger_rect3'0.2, {
        rotation-45,
        transformOrigin'50% 50%',
      });
 
    if (reverse === true) {
      tl.play();
    } else {
      tl.reverse();
    }
 
    setReverse(prev => !prev);
  }
 
  return (
    <SVG
      onClick={moveBurger}
      className="burgerSVG"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 120 80"
      width="40"
      height="40"
    >
      <rect className="burger_rect burger_rect1" width="100" height="10"></rect>
      <rect
        className="burger_rect burger_rect2"
        y="30"
        width="100"
        height="10"
      ></rect>
      <rect
        className="burger_rect burger_rect3"
        y="60"
        width="100"
        height="10"
      ></rect>
    </SVG>
  );
};
 
export default BurgerSVG2;
Link to comment
Share on other sites

It's a bit hard to debug without a live sample.

 

For something as simple as a GSAP instance toggle, perhaps using the component's state to do that could be a bit unnecessary. Here is a super simple example of an hamburger menu toggle in React using GSAP:

 

https://codesandbox.io/s/gsap-hamburger-toggle-menu-u07i6

 

As you can see I just use a single timeline instance and an event handler to control it's reversed state.

 

Happy Tweening!!!

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

This is really good Rodrigo, I been looking for an example like this!

I have a question regarding the fact that you are still using useEffect to put the gsap animation in. Can you help me understand why? That could be the issues with my I am having problems. I got rid of the useEffect since I thought that might not be need as I am not requiring the animation effect to run on load.

 

As a gsap newbie, do you know the difference between using gsap and TweenMax ? I see lots of tutorials using TweenMax and TimeLineMax and my thought it that the following imports both max versions?

 

import { gsap } from 'gsap';
Link to comment
Share on other sites

58 minutes ago, Aquasar said:

I have a question regarding the fact that you are still using useEffect to put the gsap animation in.

Basically in a functional component anything defined outside the return statement is created again on each re-render, therefore a reference or instance is not created and kept. In some cases if a component doesn't have a state and no props are passed to them you can keep the GSAP instances outside the state since that component will be rendered once in it's lifecycle. But if the component's state or props are updated, it will be re-rendered and you can get an error. Check the following example using the console:

 

https://codesandbox.io/s/gsap-instance-oustide-hooks-py2c6

 

As you can see I'm logging the value of the variable that holds a GSAP instance. In the first click is an object, as expected. But after the second click, the component is re-rendered and is set to null, but the initial render useEffect is not ran anymore, so we get an error. An alternative is to re-create the animation on each re-render, but that is a waste of code and resources, specially if the animation doesn't depends at all in the component's state or props. Since a menu button animation (the rotation, alpha or other properties being tweened) don't depend on the state, there is no need to create the tween on every render, just in the first one and save it to the component's state.

 

In a regular class component you can store the animation as a property of the class instance, which is far simpler, cleaner and easier to understand IMHO. The joy of using React hooks... :D

 

Happy Tweening!!!

  • Like 5
Link to comment
Share on other sites

Hi Rodrigo,

 

I upgraded my original project's code following step by step your simple React Modal Codesandbox example.
Having reached a good point, very near the final goal, a problem left to solve has risen again: as I originally thought there is a "body overflowX problem" left to manage above the modal appearance.

When the modal-underlaying page content has a grater height than the viewport's scrollbars appear in the window and if you scroll the mouse wheel the page content scrolls either, together with the modal, overflowing the viewport.

To fix and prevent this we have to "lock" the DOM's body Y axis scrolling before showing the modal, and the "unlock" it again when the modal has closed.

Here is how I'm trying to solve the issue (keep attention to lock() and unlock() functions and their position in the code):
 

const Modal = props => {

  let body = document.querySelector('body');
  let modal = null;
  let overlayer = null;
  let wrapper = null;

  const dispatch = useDispatch();
  const modalVisible = useSelector( state => state.modal );
  const [tl] = useState(gsap.timeline({
    defaults: {
      ease: "expo.out"
    },
    paused: true,
    onStart: () => lock(),
  }));

  const lock = () => {
    body.style = `
      overflowY: hidden;
    `;
    modal.style = `
      height: 0;
    `;
  }
  const unlock = () => {
    body.style = `
      overflowY: visible;
    `;
    modal.style = `
      height: 100vh;
    `;
  }

  useEffect(() => {
    gsap.set(modal, { height: 0 });
    gsap.set(wrapper, { yPercent: -80, xPercent: -50 });
    tl.to(overlayer, {
      autoAlpha: 0.85
    })
      .to(
        modal, {
          autoAlpha: 1,
        },0
      )
      .to(wrapper, {
        yPercent: -50,
        autoAlpha: 1
      })
      .reverse();
  },);

  useEffect(() => {
    if (modalVisible) lock();
    tl.reversed(!modalVisible);
  }, [tl,modalVisible]);

  const close = () => {
    unlock();
    dispatch(toggleModal(false));
  }

  return ReactDOM.createPortal(
    <div className={`${props.className} modal`} ref={e => {modal = e}}>
      <div
        className="modal-overlayer"
        onClick={() => close()}
        ref={e => {overlayer = e}} />
      <div className="modal-wrapper" ref={e => {wrapper = e}}>
        <ModalClose
          className="modal-close"
          onClick={() => close()}
        />
        { props.header && (
          <div className="modal-header">
            <h2>{props.header}</h2>
          </div>
        )}
        <div className="modal-content">
          <div className="modal-content-wrapper">
            MODAL CONTENT
          </div>
        </div>
        { props.footer && (
          <div className="modal-footer">
            FOOTER CONTENT
          </div>
        )}
      </div>
    </div>,
    document.body
  );

}

 

Triggering the modal opening I get this error:
 

TypeError: Cannot set property 'style' of null
lock
src/components/Modal.js:59
  56 |   body.style = `
  57 |     overflowY: hidden;
  58 |   `;
> 59 |   modal.style = `
     | ^  60 |     height: 0;
  61 |   `;
  62 | }
View compiled
Timeline.onStart
src/components/Modal.js:51
  48 |     ease: "expo.out"
  49 |   },
  50 |   paused: true,
> 51 |   onStart: () => lock(),
     | ^  52 | }));
  53 | 
  54 | const lock = () => {

 

What am I doing wrong?
Where's the problem to trigger the
lock() function inside the Timeline's onStart() callback?

 

Thank you very much ^^

Link to comment
Share on other sites

Basically the error is that the modal variable is null, therefore it doesn't have a style property. Your code seems quite convoluted IMHO.

 

Why you need the modal height to be 0 at startup? Using opacity: 0 and visibility: hidden in the CSS declaration is enough to prevent the modal from being accessible to the user, that's why we later use autoAlpha: 1 in GSAP, that sets the visibility to visible when the animation starts and the tweens the opacity value up to 1.

 

Finally if you want you can use the classList property to add/remove a class from the body element in the useEffect call:

useEffect(() => {
  tl.reversed(!props.show);
  if (props.show) {
    document.body.classList.add("no-scroll");
  } else {
    document.body.classList.remove("no-scroll");
  }
}, [props.show]);

The CSS would look like this:

.no-scroll {
  overflow: hidden !important;
}

I updated the sample in codesanbox to reflect those changes.

 

Happy Tweening!!!

  • Like 5
Link to comment
Share on other sites

Quote

I updated the sample in codesanbox to reflect those changes.

Excuse me Rodrigo, where do I find the sample? The older link seems to be another sample about another issue.
 

Quote

Finally if you want you can use the classList property to add/remove a class from the body element in the useEffect call:

I'm trying to understand why...  😚
Here is the code sections where I use the classList property, all of them inside my Modal component:

  const [tl] = useState(gsap.timeline({
    /* other stuff here... */
    onStart: () => document.body.classList.remove("no-scroll"),
  }));
  useEffect(() => {
    if (modalVisible) document.body.classList.add("no-scroll");
    tl.reversed(!modalVisible);
  },[modalVisible]);

 

  const close = () => {
    document.body.classList.remove("no-scroll");
    dispatch(toggleModal(false));
  }

This the result in the DOM after clicking the modal opening trigger button:

<body cz-shortcut-listen="true" class="">

🤬

^^

Link to comment
Share on other sites

Quote

Excuse me Rodrigo, where do I find the sample? The older link seems to be another sample about another issue.

Excuse me Rodrigo and Aquasar! I thought this was to thread I opened!!!
I'll continue in the right thread... 😅😅😅

Link to comment
Share on other sites

On 3/22/2020 at 2:52 PM, Aquasar said:

As a gsap newbie, do you know the difference between using gsap and TweenMax ? I see lots of tutorials using TweenMax and TimeLineMax and my thought it that the following imports both max versions?

 

TweenLite, TweenMax, TimelineLite, and TimelineMax is the old syntax. gsap is the new syntax, so use that instead.

  • Like 5
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...