Jump to content
GreenSock

Marcelo Jr.

ScrollTrigger + React Router v6.6 scroll restoration issue

Recommended Posts

Hi!

First of all, I'd like to say that I've been using GSAP for a few months now and I'm loving the experience! It allows me to get more creative and implement some impressive animations in a relatively easy way. Every time I didn't know how to do something or I was having issues with the logic that I was using I was able to solve the problem in a matter of minutes by reading further the docs or browsing the forum for similar problems. You do a great job!

This time I wasn't able to actually solve the problem by reading similar older posts - yes, I probably have read them all - or the docs, so I'm writing here for the first time.

 I created this demo at CodeSandbox to further illustrate my problem.

I'm trying to use ScrollTrigger to control a background change in a personal project that uses React + React Router v6 (in this demo I'm also using typescript, but the differences should be minimal when compared to vanilla javascript). It is my intention to scroll back up whenever the user is navigating the website, so I'm using the ScrollRestoration component from React Router to do so - I actually think this is a default behavior in most browsers now, and I also tried to use plain React to scroll back up when navigating (with useEffect and React Router's useLocation), but the result is always the same.

 

When navigating "forward" there is no issue, but when you go to the second page, scroll down a considerable amount and press the browser's return button to go back to the first page, the background color is off, usually having the color of the section that is situated in the exact 'offsetY' position of your previous position in the second page.

 

I'm already using the gsap.context() in my project, and it is my understanding that when I return the gsap.context().revert() in the useLayoutEffect when the component unmounts, all related ScrollTriggers inside of it should also be reverted too, right? I suspect that React is only scrolling back up after the component is already rendered, but somehow ScrollTrigger is not able to correctly read this position when that happens, but if you scroll down a little to trigger the "start" of the first animation, everything is back to normal... What am I doing wrong?

 

Thank you in advance!

Link to comment
Share on other sites

I haven't done a full deep dive in yet, but what I'm seeing initially is that you've got multiple ScrollTriggers nested in a single timeline, which can cause issues. I think your best bet would be to add a ScrollTrigger for each section and tie a tween to each of those. This should help prevent getting wires crossed.

  • Like 2
Link to comment
Share on other sites

@elegantseagulls thank you for the reply! I tried to do that in separate demo, it doesn't seem to solve the problem... Were you able to do it?

Link to comment
Share on other sites

I haven't had time just yet to do an in-code dive in, but make sure you are using ScrollTrigger.refresh() on router change end. Do you have a link to the updated demo?

  • Like 1
Link to comment
Share on other sites

Hi,

 

I'm unable to replicate this using Chrome and Firefox on Ubuntu 22. I tried what you mentioned. Went to the second page, came back to the first and scrolled a bit in order to progress the ScrollTrigger animation a bit. Then used the forward button and scrolled almost to the bottom, then use the back button and in the first page the scroll position and the color were the same.

 

Maybe I'm missing something here 🤷‍♂️

  • Like 1
Link to comment
Share on other sites

It's funny... I modified the code a bit in order to make further tests. First I fixated the navbar on the top of the viewport, and then even when I was navigating normally I encountered the same error. Then I changed the logic of the "scroll restoration", by removing the ready-to-use component from react router and implementing the useEffect/useLocation logic to scroll up in a parent component. After that I took @elegantseagulls's suggestion and explicitly called ScrollTrigger.refresh(), after the the gsap context inside the same useLayoutEffect that contains all the animations and the problem completely disappeared.

But when I do the same in my actual project, it is still not working...

 

@elegantseagulls I did the demo in incognito mode so it wouldn't change the "real" code, and I observed no change, but it wasn't saved though... Sorry! If it is really that important for this issue, I can create another demo and post here just with these changes.
 

@Rodrigo Thanks for the reply! What was going on, and maybe I should have explained that better, is that every time the user is looking at the second, third or fourth sections on the second page, and then pressing 'return' on the browser (that should bring the user back to the first page), although the page would scroll up to the top as intended, the color of the background, that should be the '#99f' of the first section, was another color (the same as one of the sections below). 

 

But now the demo is working, and I don't really understand why - isn't the gsap context supposed to "restart" every ScrollTrigger whenever it is remounted? Why calling ScrollTrigger.refresh() right after the context did the trick? Anyway, my real code is still not working, but the demo posted here is, so I will try investigate some more about why one is working and the other is not, and in the meanwhile I would really appreciate if someone here could explain to me what's really going on with this logic...

Link to comment
Share on other sites

4 hours ago, Marcelo Jr. said:

Anyway, my real code is still not working, but the demo posted here is

Are you positive that you're using the latest version of GSAP in your local project? That'd be my first guess at what the problem might be (not having the latest version). 

 

4 hours ago, Marcelo Jr. said:

isn't the gsap context supposed to "restart" every ScrollTrigger whenever it is remounted?

A gsap.context() is just a way to collect GSAP-related stuff (animations, ScrollTriggers, etc.) so that you can .revert() them super easily in one fell swoop. It doesn't "restart" anything. But of course if you've got one in a useLayoutEffect(), then each time that useLayoutEffect gets called, it would re-create that stuff in there. And assuming you did cleanup correctly (like return () => ctx.revert()), it will have flushed/reverted the old ones first. So effectively they may look like they get "restarted" but that's not really true - you just reverted/killed the old ones and created fresh ones. 

 

4 hours ago, Marcelo Jr. said:

Why calling ScrollTrigger.refresh() right after the context did the trick?

ScrollTrigger.refresh() is what forces all the ScrollTriggers to recalculate their start/end positions. So if you make changes to the DOM and things get moved/resized/whatever, you should tell ScrollTrigger to recalculate those by calling .refresh(). By default, it will automatically do that for you when the browser dispatches a "DOMContentLoaded" event and/or a "load" event. But if you're dynamically loading things in or using React to do routing, then obviously the browser won't dispatch those, so you need to call ScrollTrigger.refresh() when your DOM/layout is settled. 

 

Does that clear things up? 

 

I assume you've already read this article, right?:

 

 

  • Like 1
Link to comment
Share on other sites

Thanks for the reply @GreenSock!

 

23 hours ago, GreenSock said:

Are you positive that you're using the latest version of GSAP in your local project? That'd be my first guess at what the problem might be (not having the latest version). 

 

Yes, I am. I was able to figure out a way to make my real code here to work too, but it was not by using the same logic as the demo - I'll explain below.

 

23 hours ago, GreenSock said:

A gsap.context() is just a way to collect GSAP-related stuff (animations, ScrollTriggers, etc.) so that you can .revert() them super easily in one fell swoop. It doesn't "restart" anything. But of course if you've got one in a useLayoutEffect(), then each time that useLayoutEffect gets called, it would re-create that stuff in there. And assuming you did cleanup correctly (like return () => ctx.revert()), it will have flushed/reverted the old ones first. So effectively they may look like they get "restarted" but that's not really true - you just reverted/killed the old ones and created fresh ones. 

 

Yeah, sorry, that was I what meant (I'm relatively new to this)… I'm still not getting why calling ScrollTrigger.refresh() right after the "creation" of the animation worked in the demo... This function is using the same position parameters scrolling than those available when the animations were created, isn't it? The useLayoutEffect() in this case of the demo is only being read once, right?

 

23 hours ago, GreenSock said:

ScrollTrigger.refresh() is what forces all the ScrollTriggers to recalculate their start/end positions. So if you make changes to the DOM and things get moved/resized/whatever, you should tell ScrollTrigger to recalculate those by calling .refresh(). By default, it will automatically do that for you when the browser dispatches a "DOMContentLoaded" event and/or a "load" event. But if you're dynamically loading things in or using React to do routing, then obviously the browser won't dispatch those, so you need to call ScrollTrigger.refresh() when your DOM/layout is settled. 

 

Does that clear things up? 

 

It does to understand how the ScrollTrigger works under the hood and also when to use ScrollTrigger.refresh()  (if things get moved/resized/whatever), but I still don't get why call it right after the creation of the animation... Should I do this every time that I have an animation that uses ScrollTrigger?

 

23 hours ago, GreenSock said:

I assume you've already read this article, right?:

 

Oh, I did!

 

About my solution to my real project, the only thing that worked was to use useLayouEffect() to handle the scroll up when a route changes and creating the animations using the regular useEffect() - that runs a little bit after... I know that you recommend using useLayoutEffect() to create the animations and I think I understand why - the react docs even has a really useful demonstration about the difference between the two hooks, but I admit that I'm not super experienced and probably there's a better way in making sure that the animations will be created (or calling ScrollTrigger.refresh()) always after the scroll up has already "happened" and only use the useLayoutEffect() for the animations, but I really don't know how...

Link to comment
Share on other sites

2 hours ago, Marcelo Jr. said:

This function is using the same position parameters scrolling than those available when the animations were created, isn't it? The useLayoutEffect() in this case of the demo is only being read once, right?

Are you asking if React is only calling useLayoutEffect() once? I think in strict mode it actually calls it twice. Kinda weird, I know. 

 

2 hours ago, Marcelo Jr. said:

but I still don't get why call it right after the creation of the animation... Should I do this every time that I have an animation that uses ScrollTrigger?

No, that shouldn't be necessary. It's only when you need all the start/end values on the ScrollTrigger positions to get recalculated (typically when the page layout changes/resizes). @elegantseagulls was just recommending calling ScrollTrigger.refresh() after you do routing changes because obviously those would result in the DOM changing, shifting layout, etc. 

 

It's not clear to me what demo you're talking about that isn't/wasn't working - can you clarify? Your original had nested ScrollTriggers inside tweens that are in a timeline which is a no-no because it's logically impossible to have tweens controlled by both a parent timeline and the scroll position (those could be going in completely different directions). So I wonder if the heart of your confusion is actually caused by the nested stuff(?) Once I see a clear minimal demo that shows it breaking, I'm sure I'll have a better idea of what your confusion might be. 

 

I'm glad you got things working in your project! 🙌

  • Like 1
Link to comment
Share on other sites

13 hours ago, GreenSock said:

Are you asking if React is only calling useLayoutEffect() once? I think in strict mode it actually calls it twice. Kinda weird, I know. 

 

Yeah, I was... Because in my head this should be read only once, that's why I'm not understanding  why the demo works by calling refresh() on all ScrollTriggers right after the creation of the animation did the trick in the demo - I was not using the strict mode anyway.

 

13 hours ago, GreenSock said:

No, that shouldn't be necessary. It's only when you need all the start/end values on the ScrollTrigger positions to get recalculated (typically when the page layout changes/resizes). @elegantseagulls was just recommending calling ScrollTrigger.refresh() after you do routing changes because obviously those would result in the DOM changing, shifting layout, etc. 

 

Great! I think my problem was just not being sure how to properly call it, because apparently the <ScrollRestoration /> component was only scrolling up after the render.

 

13 hours ago, GreenSock said:

It's not clear to me what demo you're talking about that isn't/wasn't working - can you clarify? Your original had nested ScrollTriggers inside tweens that are in a timeline which is a no-no because it's logically impossible to have tweens controlled by both a parent timeline and the scroll position (those could be going in completely different directions). So I wonder if the heart of your confusion is actually caused by the nested stuff(?) Once I see a clear minimal demo that shows it breaking, I'm sure I'll have a better idea of what your confusion might be. 

 

I just updated the demo to reproduce the error I was talking about - just go to the second page, scroll down a fair amount and navigate back to the first page. It will be scrolled up, but the background color will be incorrect. There I also left some comments to my final best solution and to calling ScrollTrigger.refresh() after the creation of the animations, that somehow works in the demo (not my project though). It's also good to know that nesting these tweens is a bad idea... It wasn't what was causing this specific problem, but I'm not doing that again - thank you (and @elegantseagulls)!

 

13 hours ago, GreenSock said:

I'm glad you got things working in your project! 🙌

 

Yeah, thanks! But I wasn't happy with that solution, because I was probably losing performance with the useEffect(), so I replaced that solution for having another useLayoutEfect() to scroll up whenever the path changes before the creation of the animations, in the same component - that's the solution that is currently in the demo btw.

 

Well, by the end of the day I guess the solution to my problem was understanding better how React renders all components and when GSAP enters the process. I'm sorry if that is not the goal of this forum and for wasting everyone's time.

  • Like 1
Link to comment
Share on other sites

Hi @Marcelo Jr.

 

Is great to hear that you were able to solve the problem and that your app is working as you expect 🥳

 

8 minutes ago, Marcelo Jr. said:

I'm sorry if that is not the goal of this forum and for wasting everyone's time.

Nothing to be sorry about here, no waste of time at all. We're all teachers and students in this forums and everyone gets treated with the utmost respect and consideration. There are no bad questions here. In this thread you get to experience all that as you were both learning and at the end teaching. I'm 100% sure that at some point another user will come around this thread and implement the solution you posted and will thank you for sharing it. That's the culture in the GreenSock forums.

 

Thanks for sharing and let us know if you have other questions.

 

Happy Tweening!

  • Thanks 1
Link to comment
Share on other sites

52 minutes ago, Marcelo Jr. said:

I just updated the demo to reproduce the error I was talking about - just go to the second page, scroll down a fair amount and navigate back to the first page. It will be scrolled up, but the background color will be incorrect. There I also left some comments to my final best solution and to calling ScrollTrigger.refresh() after the creation of the animations, that somehow works in the demo (not my project though).

I'm pretty sure you don't need to call ScrollTrigger.refresh() - you can actually call the much cheaper ScrollTrigger.update() instead. It might be that there's some kind of caching at play in terms of the scroll position. When the ScrollTriggers get created in the scenario you described, the page is indeed scrolled down pretty far at that point (and then I assume your ScrollRestoration thing shoots it back up to the top). 

 

Anyway, let us know if you need anything else. Happy tweening!

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Thank you guys!

 

I did learned a lot from this. I hope it can also help somebody else in the future - as many other topics here helped me.

 

I'll try to show up from time to time if I feel I can contribute with somebody else's question, as I get more experienced.

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