0

I am using LocomotiveScroll in a React application and I have created a custom hook called useLocoScroll to handle the initialization and destruction of the LocomotiveScroll instance in order to make LocomotiveScroll work with gsap's ScrollTrigger. The hook works fine and I can use it to enable smooth scrolling and also use the ScrollTrigger in my app.

However, I now need to access the LocomotiveScroll instance from outside the hook so that I can call its methods directly. Specifically, I want to be able to call the scrollTo method of the instance to scroll to specific positions in the page, and I also want to be able to force a scroll to the top of the page whenever React Router changes routes.

I have created a function that does exactly that but it uses a new instance of the Locomotive scroll, while this worked fine if the custom hook is set to false and the first instance of the LocomotiveScroll does not exist. When the custom hook is set to true it no longer works. I am just assuming that that's because I am creating 2 instances of LocomotiveScroll.

  useLocoScroll(true);
  const { pathname } = useLocation();

  const scrollRef = useRef(null);

  useEffect(() => {
    const scroll = new LocomotiveScroll();
    scrollRef.current = scroll;

    if (scrollRef.current) {
      scrollRef.current.scrollTo('top', {
        offset: 0,
        duration: 600,
        easing: [0.25, 0.0, 0.35, 1.0],
        disableLerp: true,
      });
    }
  }, [pathname]);

I have tried adding the function to the LocomotiveScroll instance inside the hook and returning it, but I can't seem to get it to work.

This is the custom hook:

import { useState, useLayoutEffect } from 'react';
import LocomotiveScroll from 'locomotive-scroll';
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';

const useLocoScroll = (start) => {
  gsap.registerPlugin(ScrollTrigger);

  useLayoutEffect(() => {
    if (!start) return;

    const scrollEl = document.querySelector('.App');

    let locoScroll = new LocomotiveScroll({
      el: scrollEl,
      smoothMobile: false,
      smooth: true,
      multiplier: 1,
    });

    locoScroll.on('scroll', ScrollTrigger.update);

    ScrollTrigger.scrollerProxy(scrollEl, {
      scrollTop(value) {
        if (locoScroll) {
          return arguments.length
            ? locoScroll.scrollTo(value, 0, 0)
            : locoScroll.scroll.instance.scroll.y;
        }
        return null;
      },
      scrollLeft(value) {
        if (locoScroll) {
          return arguments.length
            ? locoScroll.scrollTo(value, 0, 0)
            : locoScroll.scroll.instance.scroll.x;
        }
        return null;
      },
      getBoundingClientRect() {
        return {
          top: 0,
          left: 0,
          width: window.innerWidth,
          height: window.innerHeight,
        };
      },

      pinType: document.querySelector('.App').style.transform
        ? 'transform'
        : 'fixed',
    });

    const locoScrollUpdate = () => {
      if (locoScroll) {
        locoScroll.update();
      }
    };

    new ResizeObserver(() => {
      if (locoScroll) {
        locoScroll.update();
      }
    }).observe(document.querySelector('[data-scroll-container]'));

    ScrollTrigger.addEventListener('refresh', locoScrollUpdate);
    ScrollTrigger.refresh();

    return () => {
      if (locoScroll) {
        ScrollTrigger.removeEventListener('refresh', locoScrollUpdate);
        locoScroll.destroy();
        locoScroll = null;
      }
    };
  }, [start]);
};

export default useLocoScroll;

This is what I've tried

// ...rest of the code
    const scrollToTop = () => {
      if (locoScroll) {
        locoScroll.scrollTo(0, 0, 0);
      }
    };

    ScrollTrigger.addEventListener('refresh', locoScrollUpdate);
    ScrollTrigger.refresh();

    return () => {
      if (locoScroll) {
        ScrollTrigger.removeEventListener('refresh', locoScrollUpdate);
        locoScroll.destroy();
        locoScroll = null;
      }
    };
  }, [start]);
  return scrollToTop;
};

export default useLocoScroll;

The error I receive is saying that scrollToTop is not defined.

How can I modify this hook to allow me to access and use the LocomotiveScroll instance outside the hook?

Oliver
  • 177
  • 1
  • 10

1 Answers1

1

scrollToTop is inside your useLayoutEffect, and the outer layer cannot be obtained. You can use useRef to cache this method, and then use it externally, but it will only be mounted on the ref after useLayoutEffect is executed. like:


const ref = useRef(null);

// ...rest of the code
ref.current.scrollToTop = () => {
  if (locoScroll) {
    locoScroll.scrollTo(0, 0, 0);
  }
};

ScrollTrigger.addEventListener('refresh', locoScrollUpdate);
ScrollTrigger.refresh();

return () => {
  if (locoScroll) {
    ScrollTrigger.removeEventListener('refresh', locoScrollUpdate);
    locoScroll.destroy();
    locoScroll = null;
    ref.current = null;
  }
};
}, [start]);
return ref.current;
};

export default useLocoScroll;

Sky Clong
  • 141
  • 1
  • 8
  • I did try this but I still get an error saying `Cannot set properties of null (setting 'scrollToTop')` – Oliver May 09 '23 at 12:56
  • It appears that I've resolved the issue by caching the locoScroll instead of the function, and referencing it externally like so `const scrollRef = useLocoScroll(true);`. While this solution seems to be effective within the App.jsx file, it's possible that there could be complications if I were to use it in a separate file, as it may result in a new instance of the locoScroll being initiated. – Oliver May 09 '23 at 13:12
  • If you want a `singleton mode`, you should cache this instance at the outermost layer, use the mounted instance for the first time, and then reuse the previous instance. In addition, the `scrollToTop` method call should be inside `useEffect`, so as to ensure that the scrollToTop method can be retrieved every time. I don't know if it will solve your problem – Sky Clong May 10 '23 at 01:29