Jump to content
Search Community

React and GSAP timelines callbacks

Daniel Hult test
Moderator Tag

Recommended Posts

Hi!

I had a read through your post on advanced gsap + react animations and had a question about one of the examples on component communication.

In one of your examples you're passing down timeline as a prop and then adding tweens to the timeline in the child component — which works fine!

My question is how to properly use/access the callbacks like onComplete e.g in the parent component (or in the child)? For example this onComplete will run on first render (the child component's tweens won't run first):
 

const Intro = () => {
  const [timeline, setTimeline] = useState(null);

  useEffect(() => {
    const gsapContext = gsap.context(() => {
      const tl = gsap.timeline({
        delay: 2,
        defaults: {
          duration: 2,
          ease: 'power3.out'
        },
        onComplete: () => console.log('I run immediately')
      });

      setTimeline(tl);
    });
    return () => gsapContext.revert();
  }, []);


//Later down I'm passing down the timeline to the child component and adding stuff to it
  <IntroText timeline={timeline}/>



I couldn't find a good solution to this in the forum (at least where I've looked!). Do you have any recommendations for this?
 

 

Link to comment
Share on other sites

Hi,

 

Ideally create your timeline paused or reversed and either use some event to toggle the reversed state or play it after adding all the child instances.

 

I created a simple example:

https://stackblitz.com/edit/gsap-react-basic-f48716-utkxi9?file=src%2Fcomponents%2FBox.js,src%2FApp.js

 

Feel free to fork it and adapt it to look like your app's setup, since it's far easier for us to solve issues with a minimal demo. Also we have this collection of starter templates for our users:

https://stackblitz.com/@GreenSockLearning/collections/gsap-react-starters

 

Let us know if you have more questions.

 

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

  • 2 months later...

Hey Daniel,

 

There are a few ways to tackle this that I can think of. I never actually created an app that behaves like that. It's a bit of an odd scenario since in React child components are rendered first, so when the timeline instance is set into the parent's state, that will trigger a re-render of the child components. This can be a very tricky situation since the parent component doesn't really know when the animations are created and added. You could track the timeline's duration but that's no guarantee. The best approach IMHO would be to include a callback in the parent component that allows the child component to notify that the process (creating and adding GSAP instances) is completed and somehow track the number of times that the callback should be called.

 

Other option is doing the same with a React Context wrapper to basically track the same situation.

 

@elegantseagulls mentioned creating animation components as well in another thread, maybe He can offer some advice in this particular subject.

 

I'll circle back to this a bit later and see what I can come up with to help.

 

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

3 hours ago, Daniel Hult said:

How would you play the timeline after all the child instanses are created without the interaction like clicking the button?


Removing the button and taking the paused: true off the animation seems to play the animation just fine.

The nature of setting things up like this, it's hard for React to know how many children/sub-children there are. That said, we set something similar to this up a long time ago, but I'm having trouble re-piecing it together in my head... this was when we were using pure components and lifecycles instead of hooks.

  • Like 1
Link to comment
Share on other sites

4 hours ago, elegantseagulls said:


Removing the button and taking the paused: true off the animation seems to play the animation just fine.

The nature of setting things up like this, it's hard for React to know how many children/sub-children there are. That said, we set something similar to this up a long time ago, but I'm having trouble re-piecing it together in my head... this was when we were using pure components and lifecycles instead of hooks.

Yes, the animation plays fine, but the onComplete callback runs on first render and then again once the child tweens are done unfortunately.

Link to comment
Share on other sites

6 hours ago, Rodrigo said:

Hey Daniel,

 

There are a few ways to tackle this that I can think of. I never actually created an app that behaves like that. It's a bit of an odd scenario since in React child components are rendered first, so when the timeline instance is set into the parent's state, that will trigger a re-render of the child components. This can be a very tricky situation since the parent component doesn't really know when the animations are created and added. You could track the timeline's duration but that's no guarantee. The best approach IMHO would be to include a callback in the parent component that allows the child component to notify that the process (creating and adding GSAP instances) is completed and somehow track the number of times that the callback should be called.

 

Other option is doing the same with a React Context wrapper to basically track the same situation.

 

@elegantseagulls mentioned creating animation components as well in another thread, maybe He can offer some advice in this particular subject.

 

I'll circle back to this a bit later and see what I can come up with to help.

 

Happy Tweening!


I've tried a couple of things as well, but haven't found an elegant solution yet. It's very annoying to work around annoying React problems when you're just trying to do some cool animations 😣
 

Link to comment
Share on other sites

5 hours ago, Daniel Hult said:

Yes, the animation plays fine, but the onComplete callback runs on first render and then again once the child tweens are done unfortunately.

Good find! That was actually a bug that I've now patched in the next release which you can preview at 

https://assets.codepen.io/16327/gsap-latest-beta.min.js

 

Basically if you had an onComplete on a timeline that was created inside a context that you then revert() (before that onComplete fires), the onComplete may still fire. Sorry about any confusion there. You can resolve that by just calling kill() on that timeline in your cleanup function. 

 

So to be clear, the onComplete that was firing wasn't even from your "real" timeline - React was doing its usual double-invocation of the useEffect()/useLayoutEffect() in strict mode (annoying!), so that first/extra one that was almost immediately reverted is the one that was having its onComplete fired. The 2nd one (the "real" one) was behaving as you expected. 

 

Here's a fork with that fix in place: 

https://stackblitz.com/edit/gsap-react-basic-f48716-tdyomt?file=src%2Fcomponents%2FBox.js,src%2FApp.js

 

Does that work the way you'd expect? 

  • Like 2
Link to comment
Share on other sites

6 hours ago, GreenSock said:

Good find! That was actually a bug that I've now patched in the next release which you can preview at 

https://assets.codepen.io/16327/gsap-latest-beta.min.js

 

Basically if you had an onComplete on a timeline that was created inside a context that you then revert() (before that onComplete fires), the onComplete may still fire. Sorry about any confusion there. You can resolve that by just calling kill() on that timeline in your cleanup function. 

 

So to be clear, the onComplete that was firing wasn't even from your "real" timeline - React was doing its usual double-invocation of the useEffect()/useLayoutEffect() in strict mode (annoying!), so that first/extra one that was almost immediately reverted is the one that was having its onComplete fired. The 2nd one (the "real" one) was behaving as you expected. 

 

Here's a fork with that fix in place: 

https://stackblitz.com/edit/gsap-react-basic-f48716-tdyomt?file=src%2Fcomponents%2FBox.js,src%2FApp.js

 

Does that work the way you'd expect? 


Yeah, I suspected something like that, but was unsure what the best approach would be. Your forked demo works as expected! Thank you for looking into it 😊 When can I expect that to be released?

This whole thread has been a pleasant "you're not an idiot" for me 😅
 

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

11 hours ago, Daniel Hult said:

When can I expect that to be released?

I can't give you a date, but it should be within the next couple of weeks. In the meantime, it should be pretty simple to do the kill() on the timeline like I showed in the demo (in the cleanup function). 👍

Link to comment
Share on other sites

  • 3 months later...

Just coming back to this (again) with a question:

The issue is the same if I use useEffect instead of useLayoutEffect. Is there a way to solve this using useEffect as well? With useEffect it's 50/50 chance it's working as expected.

Using gsap version 3.12.2 and NextJS.


Here is all the code essentially for the component. The issue is that the <Hero/> component renders straight away because the onComplete is fired — setting the state to true.

 

const Home = () => {
  const [loaderFinished, setLoaderFinished] = useState(false);
  const [timeline, setTimeline] = useState();

  useEffect(() => {
    const context = gsap.context(() => {
      const tl = gsap.timeline({
        onComplete: () => setLoaderFinished(true),
      });
      setTimeline(tl);
    });

    return () => context.revert();
  }, []);

  return (
    <main>{loaderFinished ? <Hero /> : <Loader timeline={timeline} />}</main>
  );
};

 

Link to comment
Share on other sites

That sounds like just a logic issue (if I understand you correctly - it's a little tricky without a minimal demo like a Stackblitz). You're creating an empty timeline, so on the very next tick (when GSAP updates), it'll say "oh, this timeline finished" (because it's completely empty, thus its duration is 0). So it SHOULD fire that onComplete in that case. The 50/50 chance you mentioned is likely just because depending on your machine's/network's performance, sometimes a tick doesn't elapse by the time you populate your timeline and the duration isn't 0 anymore. 

 

If you can't populate your timeline in the same spot where you're creating it, then perhaps you could use some simple conditional logic like: 

const tl = gsap.timeline({
  onComplete: () => tl.duration() && setLoaderFinished(true),
});

So it'll only fire if the duration isn't zero. 

Link to comment
Share on other sites

  • 2 weeks later...

Yeah, I guess the issue is useEffect runs before the DOM is ready, whereas useLayoutEffect runs after.
The reason useLayoutEffect works then is because I'm passing the timeline as a prop to the <Loader/> component, populating the timeline and having a duration before the useLayoutEffect runs.

The reason I don't populate the timeline in the same component is because I want to have a master timeline in a parent component where I can use the timeline callbacks for other stuff that's supposed to happen (e.g hide a component, fetch new data etc...).

I'm just trying to figure out the best way to work with gsap in React, as it's quite different from just a barebone frontend setup, and maybe someone can find some help from this post as well 👍🏻


I appreciate all your effort in these forums 🙏🏻

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