Jump to content
Search Community

ScrollTrigger.refresh() not working upon changing route in NextJS

claudiugubernu test
Moderator Tag

Go to solution Solved by claudiugubernu,

Recommended Posts

Hi,

 

I am using GSAP for a NetxJS project and I am having an issue with the positioning of my markers when I change the route. I tried to use the refresh() function when the routeChangeStart has occurred but that doesn't seem to be doing anything. 

 

If I manually refresh the page then my markers are set correctly.

 

import Image from "../atoms/Image";
import Paragraph from "../atoms/Paragraph";
import { useRef, useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import { useRouter } from "next/router";
gsap.registerPlugin(ScrollTrigger);

const ScrollRevealImageBlock = ({
  text,
  src,
  fallbackSrc,
  src2,
  fallbackSrc2,
}) => {
  const divRef = useRef(null);
  const imageFrontRef = useRef(null);
  const imageBehindRef = useRef(null);
  const imagesContainer = useRef(null);

  useEffect(() => {
    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: imageFrontRef.current,
        start: "center center",
        pin: imagesContainer.current,
        pinSpacing: false,
        pinnedContainer: imagesContainer.current,
        endTrigger: divRef.current,
        end: "bottom bottom",
        scrub: 1,
        markers: true,
      },
    });

    tl.to(imageFrontRef.current, {
      opacity: 1,
    })
      .to(imageBehindRef.current, {
        opacity: 0,
      })
      .to(imageBehindRef.current, {
        opacity: 1,
      });
  });

  // when the route(page) changes re-run the scrollTrigger so the markers align properly
  const router = useRouter();

  useEffect(() => {
    const pageChanged = () => {
      console.log("refreshed");
      ScrollTrigger.refresh();
    };
    router.events.on("routeChangeStart", pageChanged);
  }, [router.events]);

  return (
    <div ref={divRef} className="scroll-reveal-image-block">
      <div className="left-container site-width w-100 flex justify-end">
        {text && <Paragraph content={text} />}
      </div>
      <div ref={imagesContainer} className="right-container">
        <div ref={imageFrontRef} className="image-front image">
          <Image src={src} fallbackSrc={fallbackSrc} />
        </div>
        <div ref={imageBehindRef} className="image-behind image">
          <Image src={src2} fallbackSrc={fallbackSrc2} />
        </div>
      </div>
    </div>
  );
};

export default ScrollRevealImageBlock;

Many thanks, 

 

Klaus

Link to comment
Share on other sites

It's pretty tough to troubleshoot without a minimal demo - the issue could be caused by CSS, markup, a third party library, your browser, an external script that's totally unrelated to GSAP, etc. Would you please provide a very simple CodePen or CodeSandbox that demonstrates the issue? 

 

Please don't include your whole project. Just some colored <div> elements and the GSAP code is best (avoid frameworks if possible). See if you can recreate the issue with as few dependancies as possible. If not, incrementally add code bit by bit until it breaks. Usually people solve their own issues during this process! If not, then at least we have a reduced test case which greatly increases your chances of getting a relevant answer.

 

Here's a starter CodePen that loads all the plugins. Just click "fork" at the bottom right and make your minimal demo

See the Pen aYYOdN by GreenSock (@GreenSock) on CodePen

 

If you're using something like React/Next/Nuxt/Gatsby or some other framework, you may find CodeSandbox easier to use. 

 

Once we see an isolated demo, we'll do our best to jump in and help with your GSAP-specific questions. 

Link to comment
Share on other sites

I’m not familiar with Next and I’m not at my computer right now, but from a quick glance…

  1. Make sure that you kill() all the old ScrollTrigger instances when you reroute. You don’t want those sticking around. 
  2. It looks like you set pinnedContainer to the exact same element as trigger which seems very strange to me. You should set the pinnedContainer to the container element that’s pinned by some other ScrollTrigger (if that’s truly what you’re doing).
  3. Make sure you call ScrollTrigger.refresh() AFTER the DOM has settled (no more layout changes after the rereoute) 
Link to comment
Share on other sites

Hi @claudiugubernu and welcome to the GreenSock forums!

 

Indeed Jack is right, what you have to do is use GSAP Context in your component in order to wrap all your GSAP instances there and in the cleanup method of the component, just revert the context and that's it. This seems to be working as you expect:

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: imageFrontRef.current,
        start: "center center",
        pin: imagesContainer.current,
        pinSpacing: false,
        pinnedContainer: imagesContainer.current,
        endTrigger: divRef.current,
        end: "bottom bottom",
        scrub: 1,
        markers: true,
      },
    });
    tl.to(imageFrontRef.current, {
      opacity: 1,
    })
      .to(imageBehindRef.current, {
      opacity: 0,
    })
      .to(imageBehindRef.current, {
      opacity: 1,
    });
  });
  return () => ctx.revert();
});

Give it a try and let us know how it works.

 

Happy Tweening!

  • Like 2
Link to comment
Share on other sites

Hi both,

 

Thank you for your quick reply.

 

To answer to Jack i have pinned that same element because i want the scroll trigger to start when the whole image is in view and that seemed to me like the best way to achieve it. (a beginner move maybe?) 

 

@Rodrigo I have tried your solution but no luck. I Have added it to my codesandbox added originally.

 

Regards

 

 

Link to comment
Share on other sites

Hi,

 

You have to wait for the image to be loaded or give the image a specific height in order to avoid the issue. Basically ScrollTrigger calculations are done before the image is loaded so once the image is ready, there is a layout shift that causes the issue.

 

This seems to work:

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    imageFrontRef.current.children[0].addEventListener("load", () => {
      const tl = gsap.timeline({
        scrollTrigger: {
          trigger: imageFrontRef.current,
          start: "center center",
          pin: imagesContainer.current,
          pinSpacing: false,
          pinnedContainer: imagesContainer.current,
          endTrigger: divRef.current,
          end: "bottom bottom",
          scrub: 1,
          markers: true,
        },
      });
      tl.to(imageFrontRef.current, {
        opacity: 1,
      })
        .to(imageBehindRef.current, {
        opacity: 0,
      })
        .to(imageBehindRef.current, {
        opacity: 1,
      });
    });
  });
  return () => ctx.revert();
});

Let us know if you have more questions.

 

Happy Tweening!

Link to comment
Share on other sites

Hi Rodrigo, 

 

Thank you so much for that. While this works on my codesandbox, when i added it into my project it doesn't work.

That if check never passes. 

I've added a setTimeout instead but not sure how reliable is that.

 

 useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      setTimeout(() => {
        const tl = gsap.timeline({
          scrollTrigger: {
            trigger: imageFrontRef.current,
            start: "center center",
            pin: imagesContainer.current,
            pinSpacing: false,
            pinnedContainer: imagesContainer.current,
            endTrigger: divRef.current,
            end: "bottom bottom",
            scrub: 1,
            markers: true,
          },
        });
        tl.to(imageFrontRef.current, {
          opacity: 1,
        })
          .to(imageBehindRef.current, {
            opacity: 0,
          })
          .to(imageBehindRef.current, {
            opacity: 1,
          });
      }, 2000);
    });
    return () => ctx.revert();
  });

 

Link to comment
Share on other sites

Hi,

 

Yeah the set timeout alternative is not the most reliable. I'm curious about the fact that the image loaded event is not working in your project. Is not working on your local dev server? Is not working on a production-ready deployed app? Have you tried creating a production build on your computer and running that in your local server (Next has that alternative just run yarn build and once that is complete run yarn start).

 

I'll try something a bit later and get back to you on this. Please let me know the specific situation where this is not working and how the production build works on your computer.

 

Happy Tweening!

Link to comment
Share on other sites

Mhh... yeah depending on the transition type, some issues could stem from that. Perhaps if you are applying some styles when the page transition happens that could interfere with the calculations ScrollTrigger makes.

 

What you could try is to create a boolean that is switched when the transition starts and then is toggled back when the transition ends, and run ScrollTrigger's calculations when the boolean is switched after the transition. That boolean could reside in the page itself, no need for a global one using context or a state management solution.

 

If you can update the codesandbox so we can take a look and see what's up.

 

Let us know if you have more questions.

 

Happy Tweening!

Link to comment
Share on other sites

Hi,

 

Indeed this is related to the route transitions, which in your case are setting a fixed height to the components and that's messing ScrollTrigger calculations. If you check the render method of each page runs just when the transition start, so that most likely is the cause of your problem.

 

Unfortunately I don't have enough time to go through all your set up and find a way to solve this, since is not extremely simple. Don't worry though since is not extremely complex neither, it will just take some time that, as I mentioned before, I don't have right now.

 

What you have to do is plug a state update in the onEntered callback in the CSS Transition component:

https://reactcommunity.org/react-transition-group/css-transition#CSSTransition-prop-onEntered

 

Then you'll have to find a way to pass that state update from your transition component to the component creating the ScrollTrigger instance, in order to check that in fact the height of the main container of the page is not 100vh and that the image has loaded. When you confirm that, you should be able to create the ScrollTrigger instance without any issues. In this case since your component tree is a bit populated I'd recommend a React context instance to pass that state update, otherwise you'll find yourself passing props and perhaps event listeners through several components (prop drilling), which is something you should avoid.

 

Sorry I can't be of more assistance, but hopefully the solution I presented doesn't sound too convoluted and you are able to implement it. Also this is not a GSAP related issue but more a way these type of libraries work.

 

Let us if you have other questions.

 

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

  • Solution
On 10/22/2022 at 12:44 AM, Rodrigo said:

Hi,

 

Indeed this is related to the route transitions, which in your case are setting a fixed height to the components and that's messing ScrollTrigger calculations. If you check the render method of each page runs just when the transition start, so that most likely is the cause of your problem.

 

Unfortunately I don't have enough time to go through all your set up and find a way to solve this, since is not extremely simple. Don't worry though since is not extremely complex neither, it will just take some time that, as I mentioned before, I don't have right now.

 

What you have to do is plug a state update in the onEntered callback in the CSS Transition component:

https://reactcommunity.org/react-transition-group/css-transition#CSSTransition-prop-onEntered

 

Then you'll have to find a way to pass that state update from your transition component to the component creating the ScrollTrigger instance, in order to check that in fact the height of the main container of the page is not 100vh and that the image has loaded. When you confirm that, you should be able to create the ScrollTrigger instance without any issues. In this case since your component tree is a bit populated I'd recommend a React context instance to pass that state update, otherwise you'll find yourself passing props and perhaps event listeners through several components (prop drilling), which is something you should avoid.

 

Sorry I can't be of more assistance, but hopefully the solution I presented doesn't sound too convoluted and you are able to implement it. Also this is not a GSAP related issue but more a way these type of libraries work.

 

Let us if you have other questions.

 

Happy Tweening!

Hey Rodrigo, 

 

Many many thanks for pointing that out. I already had an appContext that was passing state through so adding a new state to keep track of the transition was easy enough to do.

 

Code for my transition group: 

<div className="relative">
    <TransitionGroup
        className={transitionState ? "transitioning" : ""}
        component={null}
      >
        <CSSTransition
          key={route}
          classNames="page"
          timeout={1000}
          onEnter={() => {
            playTransition();
            setTransitionEnded(false);
          }}
          onExited={() => {
            stopTransition();
            setTransitionEnded(true);
          }}
        >
          <MainComponent routingPageOffset={routingPageOffset}>
            <SecondaryComponent className="page-transition-inner">
              {children}
            </SecondaryComponent>
          </MainComponent>
        </CSSTransition>
      </TransitionGroup>
    </div>

And on the block itself:

// Context import
import { AppBackgroundContextState } from "../../context/AppBackgroundContextProvider";

// Context use
const { transitionEnded } = AppBackgroundContextState();

// Adding gsap.context
useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      if (transitionEnded) {
        const tl = gsap.timeline({
          scrollTrigger: {
            trigger: imageFrontRef.current,
            start: "center center",
            pin: imagesContainer.current,
            pinSpacing: false,
            pinnedContainer: imagesContainer.current,
            endTrigger: divRef.current,
            end: "bottom bottom",
            scrub: 1,
            markers: true,
          },
        });
        tl.to(imageFrontRef.current, {
          opacity: 1,
        })
          .to(imageBehindRef.current, {
            opacity: 0,
          })
          .to(imageBehindRef.current, {
            opacity: 1,
          });
      }
    });
    return () => ctx.revert();
  }, [transitionEnded]);

Again, thank you for your support :) 

 

Best, Claudiu

 

 

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