Jump to content
GreenSock

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

React + Gsap - animating menu visibility based on state

Recommended Posts

Hello everyone,

 

since recently I started using React with GSAP. I have little experience in both, so the struggle is real.

I am trying to create something very simple, a full screen menu that you can show/hide.

When i am trying to debug this, i can see the state changing, and if I try to print within the `toggleMenu()`, method, i can see the state is changing however `timeline.play()`, simply doesn't do anything. I tried also removing the if case for play and reverse but still no effect. Its just atht my timeline doesnt want to play.

 

I also tried using `componentDidUpdate()` but still no success.

 

in that case my code looked like this:

  toggleMenu() {
    this.setState({
      visible: !this.state.visible
    });
  }

  componentDidUpdate(prevProps, prevState){
    const {visible: preVisible} = prevState;
    const { visible } = this.state;

    if (visible && visible !== preVisible) {
      this.timeline.play();
    }
  }

  componentDidMount(){
    this.timeline = new TimelineLite({ paused: true })
      .to(this.menuRef.current, 0.5, {autoAlpha: 1});
      // .staggerTo(this.listNodes, 0.3, { opacity: 1,y: 0}, 0.2, "-=0.3");
  }

 

This is my container, where the magic is supposed to happen:

import React, { Component } from "react";
import Hamburger from "./hamburger.jsx";
import Menu from "./menu.jsx";
import Logo from "./misc/Logo.jsx";
import { TweenMax, TimelineLite, CSSPlugin } from "gsap/all";
import { Transition } from "react-transition-group";

class MenuContainer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      visible: false
    };
    this.toggleMenu = this.toggleMenu.bind(this);
    this.timeline = null;
    this.menuRef = React.createRef();
  }

  toggleMenu() {
    console.log(this.state.visible)
    this.state.visible ? this.timeline.reverse() : this.timeline.play();
    this.setState({
      visible: !this.state.visible
    });
  }

  componentDidMount(){
    this.timeline = new TimelineLite({ paused: true })
      .to(this.menuRef.current, 0.5, {autoAlpha: 1});
      // .staggerTo(this.listNodes, 0.3, { opacity: 1,y: 0}, 0.2, "-=0.3");
  }

  render() {
    const { visible } = this.state;
    let logoColor = "#3e444e"; //blue
    if (visible) logoColor = "#ffffff"; //White

    return (
      <div className="nav-wrapper">
        <div className="top-bar">
          <div className="top-bar-left">
            <Logo logocolor={logoColor} />
          </div>
          <div className="top-bar-right">
            <div>
              <Hamburger
                handleMouseDown={this.toggleMenu}
                menuVisibility={visible}
              />
            </div>
          </div>
        </div>
        <div>
          <Menu visible={visible} menuRef={this.menuRef} />
        </div>
      </div>
    );
  }
}

export default MenuContainer;

 

This is the menu element where I am setting the `ref`:

import React, { Component } from "react";
import { apiUrl } from './../../methods';

class Menu extends Component {
  constructor(props) {
    super(props);

    this.state = {
      links: [],
      isLoading: false,
      error: null
    };
  }
  
  componentDidMount() {

    const targetUrl = apiUrl("/sapi/main_menu_items.json");
    this.setState({ isLoading: true });
    fetch(targetUrl)
      .then(response => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error("Something went wrong....");
        }
      })
      .then(data => this.setState({ links: data.links, isLoading: false }))
      .catch(error => this.setState({ error, isLoading: false })); //Passing the data to state.links and changing isLoading to false
  }

  render() {
    const { links, isLoading, error } = this.state;

    const listItems = links.map((link, index) => (
      <li
        className="menu-link v-link"
        key={link.slug}
        // ref={li => (this.listNodes[index] = li)}
      >
        <a href={link.slug}>{link.title}</a>
      </li>
    ));

    if (error) return <p>{error.message}</p>;

    if (isLoading) return <p>Loading....</p>;

    return (
      <div className="full-screen-menu" ref={this.props.menuRef}>
        <div className="grid-y grid-frame align-middle">
          <div className="cell auto links-wrapper">
            <div className="grid-container full-height">
              <div className="grid-x align-middle full-height">
                <div className="cell small-12">
                  <ul className="links">{listItems}</ul>
                </div>
              </div>
            </div>
          </div>
          <div className="cell shrink">
            <div className="grid-container">
              <div className="grid-x">
                <div className="cell small-12 text-right text-white">
                  
                  <ul>
                    <li>
                      <a className="text-white">+31 638 501 270</a>
                    </li>
                    <li>
                      <a className="text-white">hello@whale-agency.com</a>
                    </li>
                  </ul>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default Menu;

 

And here is the hamburger, that is being clicked:

import React, { Component } from "react";
import MenuOpener from "./misc/menuOpener.jsx";
import MenuCloser from "./misc/menuCloser.jsx";
class Hamburger extends Component {
  render() {
    if (!this.props.menuVisibility) {
      return (
        <MenuOpener
          className="menu-button"
          onMouseDown={this.props.handleMouseDown}
        />
      );
    } else {
      return (
        <MenuCloser
          className="menu-button"
          onMouseDown={this.props.handleMouseDown}
        />
      );
    }
  }
}

export default Hamburger;

 

Last but not least, the important css:

.full-screen-menu {
  position: absolute;
  opacity: 0;
  visibility: hidden;
  width: 100vw;
  height: 100vh;
  bottom: 0;
  left: 0;
  background: url(/images/menu-background.jpg);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 999;
}

 

Hopefully someone can help me go in the right direction.

 

Greetings,

Nikolay

 

Link to comment
Share on other sites

Hi and welcome to the GreenSock forums.

 

The problem in your app is that you're setting a ref in a component instance and not a DOM element in your JSX:

 

<Menu visible={visible} menuRef={this.menuRef} />

 

That particular reference will return a React Component instance and not a DOM node. In fact you should be seeing an error in your console, because a component instance doesn't have a current property, so this.menuRef.current should be undefined and GSAP should be complaining about it saying that it cannot tween a null target.

 

The solution is not complicated at all. You have to create the animation logic inside your menu component and since you're already passing the visible state property as a prop to the Menu component you can listen for changes in the props in the menu component:

// in the menu component
componentDidMount () {
  this.menuTween = TweenMax.to(this.menuRef, 1, {
    autoAlpha: 1
  }).reverse();
}

componentDidUpdate (prevProps) {
  if ( prevProps.visible !== this.props.visible ) {
    this.menuTween.reversed(!this.props.visible);
  }
}

 

Here is an oversimplified example:

 

https://stackblitz.com/edit/react-gsap-togglemenu-sample

 

Happy Tweening!!

  • Like 3
Link to comment
Share on other sites

Hi Rodrigo,

 

thank you very much for clarifying everything. I was expecting its an issue with the ref, however I am new to both React and GSAP so debugging was a little nightmare. I have now managed to make it work.

To add to the answer, I don't understand why, but it was an issue when I was trying to create the TweenMax before making a `fetch()` inside `componenetDidMount()`. Only once I moved the Tween inside `.then()` I it started working again.

 

Thanks again for the help.

 

Greetings,

Nikolay "Donkoko"

Link to comment
Share on other sites

 

 

Well, basically that has to do with dealing with asynchronous code. Since you're getting your app's data in the componentDidMount() event handler that code is executed and keeps waiting for an answer from the server and the rest of the code is executed regardless of that, hence the code is asynchronous. When you move your GSAP code to the .then() method of the promise returned by fetch() that is executed when the server sends a successful response, therefore you have the data and you can use it in your GSAP powered animations.

 

Another alternative is to use async/await, but if you're not too familiar with promises it might come as a bit confusing. Basically async await allows you to write asynchronous code in a synchronous fashion, without the .then() and catch() methods.

 

You can check this resources to learn about it:

 

https://javascript.info/async-await

 

And of course the crazy guy from Sweden (note, not every person in Sweden is crazy and maybe this guy isn't as well ;) ):

 

 

 

Happy Tweening!!

 

  • Like 2
Link to comment
Share on other sites

Thanks so much for the detailed explanation.

 

I am still quite new to the JS world so it takes me some time to realize this. After posting I realized that it has to do with async/await. I will look further into it as I think it will be needed in the future. Thanks again and have a great weekend.

 

Happy Tweening!!

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