Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
rnrh

ScrollTrigger Unexpected Behavior with Smooth Scrollbar in React/NextJs

Recommended Posts

I am having an issue I have seen described in other contexts but haven't found a solution for. My project is using the Next.js framework for react, Smooth Scrollbar, and ScrollTrigger. I have a .to() with a scrollTrigger object inside that is in a <Navigation /> component. The text at the top of the page is just supposed to pin itself and scroll with the window, but it only works in some cases. I think I'm not fully understanding some lifecycle thing here. All these methods of reproducing my errors are also commented at the top of the index.js file in the sandbox

 

  1. With Smooth Scrollbar enabled, the scrollTrigger object stored in the <Navigation /> component, and the <Navigation /> component imported and nested in the main functional component, the effect does not work after refreshing the page. If you go into the scrollTrigger object in the <Navigation /> component and update one of the values (change the 'end' value to a different number) this will trigger a hot reload without refreshing the browser. If you scroll back to the top and scroll down, you will see the effect is now working.
  2. If you comment out the entire useEffect() hook in the main index.js component this will disable Smooth Scrollbar. If you save the file and refresh you will see the effect working as intended, without having to trigger a hot reload, but now there is no Smooth Scrollbar.
  3. If you reset the sandbox to the original state, uncomment the HTML in the main index.js components return, comment out the <Navigation /> component right above it, save and then refresh, you will see that the effect works on load, with Smooth Scrollbar, without having to trigger a hot reload, but now I've lost the ability to nest components.

 

CodeSandbox link

Direct link to results in browser (A little easier for refreshing)

 

Any help would be greatly appreciated. I'm out of ideas at this point.

 

 

Link to comment
Share on other sites

I'm not a React person I'm afraid, but if you look at the console you're trying to animate a null target - 
 

GSAP target .Navigation_logo__3pAtB not found.

I couldn't actually find this anywhere in your code - but maybe I just missed it and the component isn't mounted yet?

Maybe @Rodrigo or @Blake will be able to assist?

But until then - this thread may help


Also this blog post 

 

Link to comment
Share on other sites

It looks to me like you're trying to add/setup the ScrollTrigger before the component is rendered. For setting up ScrollTriggers in React, I find it best to use useCallback with the node, or parent node as a ref.

 

Simplified:
 

const nodeRef = useCallback((node) => {
  if (!node) return; // make sure element exists
  
  gsap.registerPlugin(ScrollTrigger);
  
  const logo = node.querySelector('.logo')
  
  gsap.to(logo, {
    opacity: 1,
    x: 100,
    scrollTrigger: {
      trigger: node,
      pin: true,
      end: 1000,
      pinSpacing: false
    }
  });
}, []) 

return (
 <div ref={nodeRef} >
  <div className="logo" />
 </div>
)

 

  • Like 3
Link to comment
Share on other sites

Thanks Ryan! 🥳

  • Like 1
Link to comment
Share on other sites

@elegantseagulls thank you for taking a look!

 

Would you be able to look at this CodeSandbox and see if I've implemented this as you suggested? I'm having the same issue where I can only get the effect on hot reload.

Link to comment
Share on other sites

It looks correct, but you'll want to do the same for the index.js too.

Link to comment
Share on other sites

Sorry - I'm not sure I understand what you mean. Where I'm defining all my smooth scrolling and scroll trigger stuff in index.js needs to be in a useCallback hook instead of a useEffect hook?

Link to comment
Share on other sites

Hey @rnrh,

 

Yes, useCallback would be my approach, I'm guessing that the scroller is being defined before it's rendered as well, or if you are using it in a useEffect, make sure you have a check to ensure that that scroller element is setup and exists.

Link to comment
Share on other sites

Hey @elegantseagulls

 

Unfortunately, I'm having the same issue if I wrap the smooth scrollbar and scrolltrigger setup inside a useCallback - I can only trigger on hot reload. I'm new to hooks and react so I apologize if I'm missing something really obvious here.

 

Here is a CodeSandbox of where I'm currently at.

Link to comment
Share on other sites

Hi,

 

You could try using a state property that should be updated after the smooth scroll instance is created, then in a different useEffect hook, create the ScrollTrigger instance after that state property is updated:

const [scrollBarCreated, setScrollBarCreated] = useState(false);

// This could also be a use callback as Ryan suggests
// but use effect should work
useEffect(() => {
  Scrollbar.init();
  setScrollBarCreated(true);
  // remember to cleanup your instances here
  return () => {};
}, []);

useEffect(() => {
  if(scrollBarCreated) {
    ScrollTrigger.scrollerProxy(".scroller", {
      scrollTop(value) {
        if (arguments.length) {
          bodyScrollBar.scrollTop = value;
        }

        return bodyScrollBar.scrollTop;
      }
    });
  }
  // remember to cleanup your instances here
  return () => {};
}, [scrollBarCreated]);

Another alternative would be to create a setTimeout and wait for the smooth scrollbar to be completely created before creating the scroll trigger instance. I know is far from being the react-way, but that would definitely pinpoint the issue to the DOM not being ready when the GSAP code is executed. If that does work, then you'll need some way do that check in a more react-ish way. This was discussed in this thread some time ago:

 

Another alternative is too use useLayoutEffect and see in there how things are changing and if at any stage during those changes, it's safe to create the scroll trigger instance.

 

Happy Tweening!!!

  • Like 2
Link to comment
Share on other sites

42 minutes ago, elegantseagulls said:

Yes, useCallback would be my approach

 

Mind explaining that approach over using something like useEffect?

 

Link to comment
Share on other sites

55 minutes ago, OSUblake said:

Mind explaining that approach over using something like useEffect?

 

I find that a useCallback is best for just telling when an element is available to the DOM, as I've had useEffect get funny/buggy when using refs to target elements and my linter wanting those passed in as a dependency of the useEffect, or not finding the refs on initial render, so this is why I generally use useCallback for setting up animations/scrolltriggers, because with that setup I can ensure that the node exists in the dom.  If an animation is triggered/changed via state or change, or similar, I find useEffect is best letting the dependency change trigger the effect.

 

In this instance, thinking about it more, I think @Rodrigo has a good point about creating a state for when the smooth scroller is ready, and letting that dependency refresh/initialize the useEffect or useCallback to refresh the ScrollTrigger.

Also, @rnrh, make sure to gsap.registerPlugin(ScrollTrigger) in your index.js file.

  • Like 2
Link to comment
Share on other sites

@elegantseagulls @Rodrigo would either of you be able to see if you can get an approach working off of this CodeSandbox. I'm spinning my wheels a bit. I also have an issue now where the bodyScrollBar instance isn't in scope because of using the multiple useEffects(). This is probably something I need to be dealing with in the useEffect() cleanup?

 

Thanks to you both for your help so far.

Link to comment
Share on other sites

Hi,

 

Instead of creating the scrollbar instance as a constant inside a specific useEffect hook, create a reference in the scope of the component with a useRef hook, like that it will available in every method/hook in your component:

const scroller = useRef();

useEffect(() => {
  scroller.current = document.querySelector(".scroller");
  Scrollbar.init(scroller.current);
  setScrollBarCreated(true);
}, []);

Also in the last version of your codesandbox you're not setting the scroller default for scroll trigger and the code in your navigation probably won't do anything, so you might want to check that as well:

const Navigation = () => {
  useEffect(() => {
    gsap.to("." + styles.logo, {
      opacity: 1,
      x: 100,
      scrollTrigger: {
        trigger: "#pin-target",
        pin: true,
        end: 900,
        pinSpacing: false
      }
    });
  }, []);
  return (
    <div>
      <h1 className="logo">hello</h1>
    </div>
  );
};

 

Happy Tweening!!!

  • Like 3
Link to comment
Share on other sites

@Rodrigo thanks for your continued help.

 

This CodeSandbox has everything working with refs, but I'm still stuck with the hot reload issue. I can only seem to get the effect applied after updating one of the .to() values and saving. 

Link to comment
Share on other sites

Hi,

 

The main problem here is that the code in the Navigation component is running before everything is being updated after creating the smooth scroller instance, that's why this only works in a HMR because the smooth scroll instance is already created.

 

To be completely honest I haven't used any smooth scroll packages in production so I don't have a lot of knowledge in that regard, also I really don't like them, IMHO they create more problems than they solve and I really don't like anything that creates more problems that it solves in any aspect of life, but that's just a personal opinion.

 

The only solution I can come up with that is actually working is to add a timeout in the Navigation component:

const Navigation = () => {
  useEffect(() => {
    gsap.registerPlugin(ScrollTrigger);
    setTimeout(() => {
      gsap.to(".pin-item", {
        opacity: 1,
        x: 100,
        scrollTrigger: {
          trigger: ".pin-trigger",
          markers: true,
          pin: true,
          pinSpacing: false
        }
      });
    }, 50);
  }, []);

  return (
    <div className="pin-trigger">
      <h1 className="pin-item">hello</h1>
    </div>
  );
};

But this is less than ideal and every React developer will tell you the same, but honestly I can't think of any other way and I don't have a lot of time to go through this and find out.

 

You're using Next and I believe that the idea is to create an animated nav bar that animates down when the user scrolls down in order to make it visible, right? Perhaps you could create the animation in the navigation component and create the Scroll Trigger instance in the layout component where you could instantiate the smooth scroll package as well. You can do that using forward refs, you can read more about it here:

 

https://reactjs.org/docs/forwarding-refs.html

 

Here is a simple example:

 

https://codesandbox.io/s/react-hover-forward-ref-ybtgs 

 

But those are just ideas since I have no experience with these smooth scroll packages in production. Perhaps @elegantseagulls has dealt with them and could offer some advice on the subject.

 

Happy Tweening!!!

  • Like 2
Link to comment
Share on other sites

@Rodrigo, @elegantseagulls@Cassie thanks for your continued help and patience with this. I think I will reach out to the smooth scroll library maintainer and see if they have any recommendations.

 

I don't disagree with you on the smooth scroll stuff, fwiw. Unfortunately, I don't have a say over it for this project. If I find a good solution I'll come back and post it here for posterity.

 

Thanks again for your help.

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