Jump to content
Search Community

cleanup ScrollTrigger matchMedia in React

j&o test
Moderator Tag

Recommended Posts

Hi there,

 

I am using ScrollTrigger a lot, its a fantastic addition! the only issue I've run into so far is cleaning up when using ScrollTrigger.matchMedia() when un-mounting in my React components. 

I've tried killing all ScrollTrigger instances, and killing timelines individually. simplified setup below:
 

const buildTimeline = () => {
  // ... setup stuff
  ScrollTrigger.saveStyles([boxRef.current, mobileRef.current]);

  ScrollTrigger.matchMedia({
    '(min-width: 720px)': () => {
      if (!boxRef.current) {
        console.log('boxRef does not exist'); 
      }

      ScrollTrigger.create({
        // config stuff
        animation: desktopTimeline.current
          .to(
            // animations
          )
      });
    }, 
    
    '(max-width: 719px)': () => {
      if (!mobileRef.current) {
        console.log('mobileRef does not exist'); 
      }

      ScrollTrigger.create({
        // config stuff
        animation: mobileTimeline.current
          .to(
          // animations
          )
      });
    },
    
  });
  }

  useEffect(() => {
    if (!hasMounted.current) {
      hasMounted.current = true;
      buildTimeline();
    }

    return () => {
      // kill all ScrollTrigger[s]
      ScrollTrigger.getAll().forEach(t => t.kill());

      // try killing individual timelines also
      mobileTimeline.current.kill();
      desktopTimeline.current.kill();
    }
  }, []);

 

This would normally work ok  on ScrollTrigger instances generally - or at least it seems to! - but if I'm using matchMedia I'll still get media query change events firing where the component is unmounted (i.e. navigating to a different route). any idea what I'm missing here? 

Link to comment
Share on other sites

thanks for the reply Zach :)

 

It's a kind of difficult issue to do a codepen for as it is on unmount after a route change. I can probably whip up something if its unclear as to what I mean but basically using any of the .kill() methods mentioned - and ScrollTrigger.kill() -  doesn't remove ScrollTrigger -- the events are still firing on resize at the defined media queries (in the code posted, <720 & >720) even though that component is unmounted and I have attempted to kill the instance in the ways I have outlined above. 

 

I can send you a DM to a demo if that would help? 

 

Thanks again for getting back to me, I appreciate it!

Link to comment
Share on other sites

3 minutes ago, ueno said:

and ScrollTrigger.kill() -  doesn't remove ScrollTrigger

Are you sure that the method is being called? ScrollTrigger.kill() should completely kill off all of ScrollTrigger, I'd be very surprised if events are still firing afterwards.

 

Keep in mind that the static .kill() method is different than the instance .kill() method.

Link to comment
Share on other sites

Hey there Zach,

 

if i attempt to kill with ScrollTrigger.kill() on unmount, I get the following ts error:
The 'this' context of type 'ScrollTrigger' is not assignable to method's 'this' of type 'PluginScope'.
  Type 'ScrollTrigger' is missing the following properties from type 'PluginScope': _props, _pt, add

 

Is there a different way I should be calling this other than one of these methods?: 

 

import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/dist/ScrollTrigger';

useEffect(() => {
  // call function to setup ScrollTigger;
  
  return () => {
    ScrollTrigger.getAll().forEach(t => t.kill()); // no error but media query events still fire after unmount
    ScrollTrigger.kill(); // error, resize mq events still fire on unmount
    specificTimelineName.kill(); // no error but events still fire on unmount
  }
}, []);

 

Link to comment
Share on other sites

I am registering it at the _app level (its a next.js app). I can't really post it here but I can send you a link to a simple demo and the source repo as a DM? I would of course post any solution as a result back into the forum. 

Link to comment
Share on other sites

Make sure you're waiting to register (and use) the plugin until the window, document, and <body> exist. 

 

It would be MUCH better if you create a minimal demo rather than just linking us to your actual project. There's usually a lot of extraneous, unrelated code and/or other complexities that may be interfering in your real project. When you create a reduced test case, it often reveals the issue pretty quickly. :)

  • Like 1
Link to comment
Share on other sites

I am registering it correctly :)

 

I'll whip up a pen as soon as i can tomorrow. Let me know if I can send you a link in the meantime? it's as minimal an example as you could imagine - its the "hello world" of matchMedia

Link to comment
Share on other sites

Got the demo. I think I see the misunderstanding. 

 

It sounds like you thought that killing a ScrollTrigger that was created inside of one of the .matchMedia() functions would somehow prevent that .matchMedia() function from ever being called again, but that's not how it works. Those are totally separate concepts. Once you set up a matchMedia() at a breakpoint, it will always get called. So the solution with your project would be to just set a variable that you can check inside that function, sorta like:

 

let killed;

ScrollTrigger.matchMedia({
  '(min-width: 720px)': () => {
    if (!killed) { // ONLY IF NOT KILLED!
      ScrollTrigger.create(...); 
    }
  }
});

useEffect(() => {
  ...
  return () => {
      ScrollTrigger.getAll().forEach(t => t.kill());
      killed = true;
    };
}, []);

Does that clear things up? 

  • Like 3
Link to comment
Share on other sites

hey Jack & Zach,

 

Thanks for the assistance. Yep, I assumed that ScrollTrigger.getAll() would kill everything including ScrollTrigger.matchMedia() so I misunderstood the purpose of .kill(); thanks for clearing that up :) 

 

A boolean flag works just fine on unmount, along with killing all the individual ScrollTriggers with a loop over ScrollTrigger.getAll() just like in your example.

 

appreciate the clarification and thanks again for your time!

 

 

  • Like 1
Link to comment
Share on other sites

  • 1 year later...
On 7/25/2020 at 7:24 AM, GreenSock said:
let killed;

ScrollTrigger.matchMedia({
  '(min-width: 720px)': () => {
    if (!killed) { // ONLY IF NOT KILLED!
      ScrollTrigger.create(...); 
    }
  }
});

useEffect(() => {
  ...
  return () => {
      ScrollTrigger.getAll().forEach(t => t.kill());
      killed = true;
    };
}, []);

 

Hi everyone

tried this approach but on mounting again the killed variable value will reset hence the condition will still fire, we somehow need to have a method to kill the matchMedia on unmount, it also cause another issue when remounting back because the previous matchMedia still exist and the scrolltrigger and tweens comes with it is already been destroyed and it throws me an error "Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'." this error will also show when the component has been unmounted and the screen size updated

Link to comment
Share on other sites

  • 1 year later...

I have the same problem with Swup instead of React.

Since ScrollTrigger.clearMatchMedia() is deprecated, what can I use to remove all instances of gsap.matchMedia()?

 

Now I'm using gsap.matchMediaRefresh(); and although I kill all scrolltriggers, when gsap.matchMedia is matching, all scrolltriggers defined inside it are being regenerated.

It seems that gsap.matchMediaRefresh(); doesn't kill the instances of gsap.matchMedia(), I probably misunderstood its function.

 

Using Swup I can't rely on page reloading to reinitialize all js, so I have to "reset" manually the state of the page killing all scrolltriggers AND matchMedias

Link to comment
Share on other sites

Hi Cassie, thank you.

Yes, eventually I changed some conditional logic and used mm.revert(); in the right place if some conditions are not met.

 

Nevertheless, is there a way to collect all instances of gsap.matchMedia and revert them, in a similar fashion to what we do with ScrollTrigger.getAll()?

Link to comment
Share on other sites

4 hours ago, XXVII said:

It seems that gsap.matchMediaRefresh(); doesn't kill the instances of gsap.matchMedia(), I probably misunderstood its function.

Yeah, that's not what gsap.matchMediaRefresh() does - that's just to force all matching ones to revert() and then re-match. Think of it like a flushing and re-triggering of any that should match currently. It doesn't actually destroy them or anything. Like if you've got a checkbox that alters things like reduced motion (or whatever, as shown in the docs), you may have a bunch of conditional logic inside your gsap.matchMedia() stuff that should run differently based on that. So even though the viewport didn't change size at all, you may still want to re-run all your stuff. 

 

3 hours ago, XXVII said:

Nevertheless, is there a way to collect all instances of gsap.matchMedia and revert them, in a similar fashion like we do with ScrollTrigger.getAll()?

I'm trying to imagine a scenario where you'd need something like this. It doesn't seem practically very useful, but I may be missing something. I just hate adding more kb and expanding the API surface area for things that almost nobody would ever use. With the new gsap.context() and gsap.matchMedia() revert functionality, this seems totally unnecessary. Even ScrollTrigger.getAll() probably isn't very useful at this point given all the new flexibility in those methods. But let me know if you can think of a practical use case where you'd really need a way to get all matchMedia() instances. 

Link to comment
Share on other sites

I don't know if you are familiar with Swup, I quote the website "Swup is an extensible and easy-to-use page transition library for server-side rendered websites. It handles the complete lifecycle of a page visit by intercepting link clicks, loading the new page in the background, replacing the content and transitioning between the old and the new page."
 

This means that if I define some scrolltriggers (indipendently or inside gsap.matchMedia()), when I click to go to another page, those "entities" are not destroyed by the reloading of the page, because there's no reloading between pages.
So I have to kill everything I initialized in the old page and initialize what I need in the new page, using the events provided by Swup: https://swup.js.org/getting-started/reloading-javascript/

What happened to me is that I easily killed the scrolltriggers using ScrollTrigger.getAll(), but the scrolltriggers created inside gsap.matchMedia() were being recreated when the mediaquery matched, even if they were killed before.

 

My code (which is called every time a new page "loads") was something like this:

let mm = gsap.matchMedia();

if ("some conditions are met")
{
    mm.add({
      isDesktop: '(min-width: 992px)',
      isMobile:  '(max-width: 767.98px)'
      
    }, (context) => {

      let {isDesktop, isMobile} = context.conditions;

      // create scrolltriggers
    });      
}
else
{
  mm.revert();
}

When the conditions were not met, mm.revert(); was called but the scrolltriggers were recreated on resize anyway.
This is what I don't understand: mm.revert() was called but the "old" mm somehow survived.

 

I solved my problem like this:

let mm = gsap.matchMedia();

mm.add({
  isDesktop: '(min-width: 992px)',
  isMobile:  '(max-width: 767.98px)'

}, (context) => {

    if ("some conditions are met")
    {
      let {isDesktop, isMobile} = context.conditions;

      // create scrolltriggers
    else
    {
      mm.revert();
    }
  });      
}

I don't know if this is the right strategy, but I moved the "if" inside the context.
Now it works but I don't know why :)

Link to comment
Share on other sites

Glad you got it working. It's tough to troubleshoot without a minimal demo to look at, but it looks to me like you just weren't doing proper cleanup. Based on your snippet, your code adds stuff to a MatchMedia...and then later (like when you exit that page and go to a new one), I assume you never called .revert() on that particular matchMedia() instance. It looks like you just create new ones all the time but never do cleanup of the old ones. Your code snippet creates one and immediately reverts it if those conditions aren't met, but that's a total waste (why even create it if you're gonna immediately revert it without adding anything?) 

 

You should make sure that when you exit that page/route/whatever, you do proper cleanup by calling revert() on that particular MatchMedia instance. 

 

In other words, it kinda looks like you were doing this:

 

function onMount(condition) {
  let mm = gsap.matchMedia();
  if (condition) {
    mm.add(...);
  } else {
    mm.revert();
  }
}

onMount(true);
onMount(false);

Both onMount() calls create an entirely new instance. The first one is NEVER cleaned up. The second one does absolutely nothing and just gets reverted right away. See the problem? 

Link to comment
Share on other sites

Hi,

 

Another user had some issues with SWUP as well, in a different scenario but still issues with refreshing and updating GSAP Instances such as ScrollTrigger and ScrollSmoother. You can also check this thread and see if you can borrow some of that user's code in order to get a better grasp of this whole ordeal:

 

 

Hopefully this helps.

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Thank you Jack, your message has pointed me in the right direction.

Also, the thread shared by Rodrigo let me know about gsap.context(), which I used to organize my code more efficiently.

 

Now my code looks like this, and it works:

export let gsapContext = gsap.context(() => {});
export let gsapMatchmedia;

/*
here I import some other files which add more animations 
and use the gsapContext and gsapMatchmedia variables created above
*/

if (/*conditions are met*/)
{
	gsapContext.add(() => {

		gsapMatchmedia = gsap.matchMedia();

		gsapMatchmedia.add({
			isDesktop: '(min-width: 992px)',
			isMobile:  '(max-width: 767.98px)'

		}, (context) => {
			// some scrollTriggers here
		});

	});
}

/* 
"willReplaceContent" triggers when a link to another page is clicked, 
right before the content of the current page is replaced with the new one
*/
swup.on('willReplaceContent', killGsapAnimations);

// kill all scrollTriggers and matchMedias defined here and in other files
function killGsapAnimations() {

	if (typeof gsapContext !== 'undefined')
	{
		gsapContext.revert();
	}

	if (typeof gsapMatchmedia !== 'undefined')
	{
		gsapMatchmedia.revert();
	}
}

 

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