Jump to content
Search Community

Asynchronous Timeline callbacks setting on REACT

Tommy81 test
Moderator Tag

Recommended Posts

Hi there,


the following is the extremely trimmed and simplified conceptual snippet of from a real project of mine where I'm having what it seem to be a scope problem.
 

const My Component = () => {

  const flagA = useSelector(state => state.flagA);
  const flagB = useSelector(state => state.flagB);

  const [tl] = useState(gsap.timeline({
    defaults: {
      // defaults setting here
    },
    paused: true,
    // here after the actual callback setting
    onReverseComplete = () => {
      if (flagA) {
        // do A stuff here
      }
      if (flagB) {
        // do B stuff here
      }
    },
  }));

  useEffect(() => {
    // tl tweening elements initial settings here
    // tl tweening code here
  },[]);

  // This is where I think I should define the onReverseComplete callback
  useEffect(() => {
    tl.onReverseComplete = () => {
      if (flagA) {
        // do A stuff here
      }
      if (flagB) {
        // do B stuff here
      }
    }
  },[flagA,flagB]);
  
  // Render stuff here
  return();

}

As you can see the component has two (boolean) vars, flagA and flagB which come from a Redux Store.
On the original and actual project's code the onReverseComplete() callback is set in the useState() definition.
The problem is that in this way the first time the callback is defined the function scope takes the initial flagA and flagB values and never updates but flagA and flagB values change through the application and I need to trigger different effects inside the onReverseComplete() callback according to flagA and flagB values.

In the code you can also find a fake useEffect() definition where i suspect I must define the onReverseComplete() callback but I'm not understanding how and if it is possibile.
I'm not understanding that reading GSAP docs.

Maybe with the Timeline add() method? If yes: how I should do that?

Thank you for answering ^^

Link to comment
Share on other sites

Hey Tommy.

 

Callback functions aren't evaluated until they are needed so it shouldn't matter when you add the callback function. I don't think using .add() later would help either.

 

With that being said, I haven't worked with React much so I don't know exactly why your variables aren't updating their values. 

@Rodrigo@OSUblake, and perhaps @elegantseagulls know the most about React around here so hopefully one of them can help you out :) 

Link to comment
Share on other sites

56 minutes ago, ZachSaucier said:

Hey Tommy.

 

Callback functions aren't evaluated until they are needed so it shouldn't matter when you add the callback function. I don't think using .add() later would help either. With that being said, I haven't worked with React much so I don't know exactly why your variables aren't updating their values. 

 

@Rodrigo@OSUblake, and perhaps @elegantseagulls know the most about React around here so hopefully one of them can help you out :) 

Hi Zach,

 

I logged and re-logged "flagA" and "flagB" (meaning the real vars in my real code) for hours making, several tests..., changes... the only explanation I can give is the one I reported: flagA and flagB appear to be "freezed" at their first/initial values in the Redux Store...

I've tried, add(), call(). registerEffect()... I'm not going anywhere... I'm stucked... and frustrated...

Link to comment
Share on other sites

For hooks, you should create your animations with useRef, like here.

 

For your onReverseComplete, I'm not sure how to do that because it looks like you are capturing flagA and flagB values in a closure, which wouldn't update. My suggestion would be to use a class to work around that. 

 

onReverseComplete = () => {
  if (this.state.flagA) {
    // do A stuff here
  }
  if (this.state.flagB) {
    // do B stuff here
  }
}

 

 

  • Like 5
Link to comment
Share on other sites

Hi,

 

While Blake's suggestion does work and around here we recommend using class components when it comes to using GSAP in a React app, you're right about updating the event callback after the store's state is updated, but you're wrong about the approach to do so.

 

The main issue is that onReverseComplete is not a property of a GSAP instance, so doing this:

const myTween = gsap.to(target, {
  onReverseComplete: () => console.log("Reverse Complete!!!")
});

myTween.onReverseComplete = () => console.log("Another reverse complete callback");

Will have no effect whatsoever, it only adds a property to the GSAP instance called onReverseComplete. So this is not about scope it's merely about the value in the original instance using a reference to older values stored in it. So in order to reflect any update to those values you can use the eventCallback method to update the callback in the GSAP instance. Here is a live sample, you can check the console to see that the callback returns the updated values in the redux store:

 

https://codesandbox.io/s/gsap-react-hooks-redux-4peeu

 

Happy Tweening!!!

  • Like 5
Link to comment
Share on other sites

@Rodrigo:

Quote

myTween.onReverseComplete = () => console.log("Another reverse complete callback");

Will have no effect whatsoever, it only adds a property to the GSAP instance called onReverseComplete.


Yes I know, I was only trying let understand my intent conceptually 😊

 

Quote

So in order to reflect any update to those values you can use the eventCallback method to update the callback in the GSAP instance
[...]

Here is a live sample, you can check the console to see that the callback returns the updated values in the redux store:
https://codesandbox.io/s/gsap-react-hooks-redux-4peeu

 

I had already tried the eventCallback() approach but it failed...
The live example your shared seems to be perfect for me but, again, I read it and compared it with my code but, again, it is still not working... can't understand WHY...
It seems all right to me...

I'm going to change the component to a class one as a last chance but I would prefer to make the hooks version works... ^^

Soon I'm going to share my real code but in this moment I've got internet connection problems and the code share tool is not going to load...

Link to comment
Share on other sites

// REACT
import React, {
  useEffect,
  useState,
} from 'react';
import ReactDOM from 'react-dom';

// REDUX
import {
  useDispatch,
  useSelector,
} from 'react-redux';
import {
  toggleModal,
} from '../redux/actions';

// STYLED COMPONENTS
import styled from 'styled-components/macro';
import { transparentize } from 'polished';

// GSAP
import { gsap } from 'gsap';

// Typing
import PropTypes from 'prop-types';

// STYLES
import {
  absolute,
  fit,
} from '../styled/mixin';

// Components
import ModalClose from './ModalClose';

// Vars
const zID = 2;

// Main component
const Modal = props => {

  const closeButtonClicked = useSelector(state => state.modal[`${props.name}_close`]);
  const dispatch = useDispatch();
  const modalVisible = useSelector( state => state.modal[props.name] );
  const name = props.name;

  let firstLoad = true;
  let modal = null;
  let overlayer = null;
  let wrapper = null;

  const onReverseCompleteCallback = () => {
    if (!modalVisible && !closeButtonClicked && !firstLoad) {
      console.log("Modal: "+props.name+" | tl reversed: modal closed by input");
      props.onCloseComplete();
    }
    if (modalVisible && closeButtonClicked && !firstLoad) {
      console.log('Modal: '+props.name+" | tl reversed: modal closed by close button");
      dispatch(toggleModal(name));
    }
    if (firstLoad) firstLoad = false;
  };

  const [tl] = useState(gsap.timeline({
    defaults: {
      duration: 0.3,
      ease: "expo.out",
      transformOrigin: "center",
    },
    onReverseComplete: onReverseCompleteCallback,
    paused: true,
  }));

  useEffect(() => {
    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(() => {
    tl.eventCallback(
      'onReverseComplete',
      onReverseCompleteCallback,
      [closeButtonClicked,firstLoad,modalVisible,props]
    );
    if (modalVisible) {
      console.log('Modal: '+props.name+" | modalVisible > "+modalVisible);
      document.body.classList.add("no-scroll");
      tl.reversed(!modalVisible);
      if (closeButtonClicked) tl.reverse();
    }
    else {
      console.log("Modal: "+props.name+" | modalVisible > "+modalVisible);
      document.body.classList.remove("no-scroll")
      if (!closeButtonClicked) tl.reverse();
      else dispatch(toggleModal(`${props.name}_close`));
    }
  },[tl,closeButtonClicked,modalVisible]);

  const close = () => {
    // console.log('Modal: '+props.name+" close button clicked");
    dispatch(toggleModal(`${props.name}_close`));
  }

  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-wrapper">
          <div className="modal-content">
            {props.children}
          </div>
        </div>
        { props.footer && (
          <div className="modal-footer"></div>
        )}
      </div>
    </div>,
    document.body
  );

}

// Styles component
const Styled = styled(Modal)`
  ${fit()}
  opacity: 0;
  position: fixed;
  left: 0;
  top: 0;
  visibility: hidden;
  z-index: ${zID};

  .modal {

    &-content {
      overflow-x: hidden;
      overflow-y: auto;

      &-wrapper {
        padding: ${props => props.theme.pad.modal};
      }

    }

    &-header {
      padding: ${props => {
        const p = props.theme.pad.modal.split('px')[0];
        return `${p*2}px ${p}px ${p}px`;
      }}
    }

    &-overlayer {
      ${absolute({ type: 'leftTop' })}
      ${fit()}
      background: ${props => transparentize(0.5,props.theme.bg.modal_overlayer)};
      opacity: 0;
      visibility: hidden;
      z-index: ${zID+1};
    }

    &-wrapper {
      background: ${props => props.theme.bg.modal_wrapper};
      border-radius: ${props => props.theme.radius.modal_wrapper};
      left: 50%;
      max-height: ${props => props.theme.h.modal_wrapper};
      top: 50%;
      opacity: 0;
      position: absolute;
      visibility: hidden;
      width: ${props => props.theme.w.modal_wrapper};
      z-index: ${zID+2};
    }

  }

`;

// Typing
Modal.propTypes = {
  className: PropTypes.string.isRequired,
  children: PropTypes.any.isRequired,
  footer: PropTypes.any,
  header: PropTypes.string,
  onCloseComplete: PropTypes.func,
}

// Default
Modal.defaultProps = {
  onCloseComplete: Function
}

// Exports
export default Styled;

Here is the full code...

Link to comment
Share on other sites

This is a problem right here:

if (modalVisible) {
  console.log('Modal: '+props.name+" | modalVisible > "+modalVisible);
  document.body.classList.add("no-scroll");
  // Problem
  tl.reversed(!modalVisible);
  if (closeButtonClicked) tl.reverse();
}

Basically you're saying if modalVisible is true execute that code, then you set the reversed() property of the timeline to the oposite of the modalVisible boolean in this case you're setting it to false, which means the timeline will go forward, right after that you check the closeButtonClicked value and if it is truthy you reverse the timeline. So basically in two lines of code, and of course depending on the values of those state properties, you're telling the timeline to go forward and immediately to go backwards, hence nothing happens.

 

A simple suggestion, a modal close method by default should set the modalVisible property to false, no need to pass the value of a property of a child component, that's just a problem waiting to happen:

const close = () => {
  // If you're closing the modal just set the paramenter to false
  // This assumes that the payload false will trigger the modal to close
  dispatch(toggleModal(false));
}

If you want to pass other values to the modal like data or something to set it's title, then is better to create a specific reducer for the modal in redux, although I prefer using composition for such cases, if possible.

 

Finally is hard to see the real issue without looking at a live sample, please provide a codesandbox so we can take a look at this.

 

Happy Tweening!!!

  • Like 4
Link to comment
Share on other sites

Hi Rogdrigo,

 

I solved the problem but not for the reason you highlight (indeed the logic is right) but just with the removal of firstLoad var which become usesless after last updates I integrated following your live example.

Considerating your kindness and effort to help me I'll explain you why your suggestion was not hitting the problem:
First of all: is not your mistake, it's only due to the fact you rightly assert that "is hard to see the real issue without looking at a live sample".
You did't have the full view 🙂
The project I'm working on is a React App which interacts with a local MySQL database and I would have to rewrite almost everything just to build a live example...

The problem has it's origin in this fact: there are two possibile scenarios by which the modal can be closed:

1. The normal closure through the close button click
2. The modal closure as the consequence of the user interaction with the modal content, WITHOUT close button click.

The flags I created have the role to manage the discrimination between these two scenarios, these tho different logic flows.

In my application's Redux store I've reserved a reducer which works on the modal node to manage the flags to discriminate these two different modal closure scenarios/flows.

The application, at a data level, works with "stores" which can be link to a "banner".
In the app there are two modals, one to create a new banner and one to select and link a banner to a store.
The first its been called "banner_creation", the last "banner_select".

Every modal has its relative flag in the Redux store to manage the type 2 scenario with more another flag to manage the type 1 scenario.
The type 1 flag just gets a "_close" suffix.

the result in the Redux store is:
 

modal: {
  banner_creation: false,
  banner_creation_close: false,
  banner_select: false,
  banner_select_close: false,
}

The type 2 scenario flag, banner_creation for example, manages the banner creation modal visibility status while the type 1 flag manages the relative modal button clicked status.


The moment I wrote complaining to not understand why It was again not working I was not referring to the modal closure but to the full execution and the two different logic flows.

This was what actually happened:
 

  const onReverseCompleteCallback = () => {
    if (!modalVisible && !closeButtonClicked && !firstLoad) {
      console.log("Modal: "+props.name+" | tl reversed: modal closed by input");
      props.onCloseComplete();
    }
    if (modalVisible && closeButtonClicked && !firstLoad) {
      console.log('Modal: '+props.name+" | tl reversed: modal closed by close button");
      dispatch(toggleModal(name));
    }
  };

When the modal did close reversing the tweening the specific callback was rightly triggered but no condition was hit because firstLoad was alway true (I'm not going to explain why now).... ^^
Removing the firstLoad var all works fine!!!! 😀😅

Thank you very much Rodrigo for your help and patience 🙂

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