useScroll(๐Ÿ“œ)

07 August 2020ย ย โ€ขย ย 1 min read

#7 hook of the week

To remind you of the purpose and goal of this week:

For a whole week I'll post a new hook every day. All hooks will support Server-Side Rendering (SSR) and have TypeScript implemented.

I will not describe the hooks in great detail, I'll just present them and show how it's implemented. Then you can do whatever you want with them.

Every hook is also available here, together with a range of other hooks.

View source

Showcase ๐Ÿ”ฅ

Here you can see the difference between no throttle and throttle with a 500 milliseconds wait timeout.

No throttle
Direction...
Throttle 500
Direction...

The hook ๐ŸŽฃ

With this hook you get scroll amount on y axis and the current direction, and with the opportunity to throttle the callback.

I'm using my isSSR utility, getRefElement() utility and useEventListener() hook to get the scroll data.

import { useState, useCallback, useMemo, RefObject } from 'react';
import { throttle } from 'lodash';
import { useEventListener } from './hooks';
import { isSSR, getRefElement } from './utils';

interface Scroll {
  y?: number;
  x?: number;
  direction?: 'up' | 'right' | 'down' | 'left';
}

interface UseScroll {
  wait?: number;
  element?: RefObject<Element> | Window | null;
}

export const useScroll = (options?: UseScroll): Scroll => {
  const { wait, element } = useMemo<UseScroll>(
    () => ({
      wait: 250,
      element: isSSR ? undefined : window,
      ...options
    }),
    [options]
  );

  const getScrollOffset = useCallback(
    (direction: 'y' | 'x') => {
      const target = getRefElement(element);

      if (isSSR || !target) {
        return undefined;
      }

      if ('window' in target) {
        return direction === 'y' ? target.pageYOffset : target.pageXOffset;
      }

      if ('nodeType' in target) {
        return direction === 'y' ? target.scrollTop : target.scrollLeft;
      }
    },
    [element]
  );

  const [scroll, setScroll] = useState<Scroll>({
    y: getScrollOffset('y'),
    x: getScrollOffset('x'),
    direction: undefined
  });

  const setDirection = useCallback(
    ({ y, x }: Scroll) => {
      const yOffset = getScrollOffset('y');
      const xOffset = getScrollOffset('x');

      if (
        y !== undefined &&
        x !== undefined &&
        yOffset !== undefined &&
        xOffset !== undefined
      ) {
        if (y > yOffset) return 'up';
        if (y < yOffset) return 'down';
        if (x > xOffset) return 'left';
        if (x < xOffset) return 'right';
      }
    },
    [getScrollOffset]
  );

  const scrollFunc = useCallback(() => {
    const yOffset = getScrollOffset('y');
    const xOffset = getScrollOffset('x');

    setScroll((prev) => ({
      y: yOffset,
      x: xOffset,
      direction: setDirection(prev)
    }));
  }, [getScrollOffset, setDirection]);

  const handleScroll = useMemo(
    () =>
      wait !== 0 ? throttle(() => scrollFunc(), wait) : () => scrollFunc(),
    [wait, scrollFunc]
  );

  useEventListener({
    type: 'scroll',
    listener: handleScroll,
    element,
    options: { passive: true }
  });

  return scroll;
};

Usage

On scroll we console.log() its direction and amount, and throttle it with a 500 milliseconds wait timeout.

import React, { useEffect, useRef } from 'react';
import { useScroll } from './hooks';

const Component = () => {
  const ref = useRef(null);
  const scroll = useScroll({
    wait: 500,
    element: ref
  });

  useEffect(() => {
    console.log(scroll);
  }, [scroll]);

  return <div ref={ref} />;
};

export default Component;

The end ๐Ÿ“œ

I hope you found this helpful.