Tommy81 Posted April 14, 2020 Share Posted April 14, 2020 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 More sharing options...
ZachSaucier Posted April 14, 2020 Share Posted April 14, 2020 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 More sharing options...
Tommy81 Posted April 14, 2020 Author Share Posted April 14, 2020 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 More sharing options...
OSUblake Posted April 14, 2020 Share Posted April 14, 2020 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 } } 5 Link to comment Share on other sites More sharing options...
Rodrigo Posted April 15, 2020 Share Posted April 15, 2020 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!!! 5 Link to comment Share on other sites More sharing options...
Tommy81 Posted April 22, 2020 Author Share Posted April 22, 2020 @OSUblake: Thank you Blake for answering. I had already tried you solution but without success. Link to comment Share on other sites More sharing options...
Tommy81 Posted April 22, 2020 Author Share Posted April 22, 2020 @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 More sharing options...
Tommy81 Posted April 22, 2020 Author Share Posted April 22, 2020 // 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 More sharing options...
Rodrigo Posted April 22, 2020 Share Posted April 22, 2020 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!!! 4 Link to comment Share on other sites More sharing options...
Tommy81 Posted April 29, 2020 Author Share Posted April 29, 2020 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 More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now