0

I wanted to calculate the user scroll height , so I created a custom hook. and I wanted to share this value to another component. but it doesnt work. code:

const useScroll = () => {
  let scrollHeight = useRef(0);

  const scroll = () => {
    scrollHeight.current =
      window.pageYOffset ||
      (document.documentElement || document.body.parentNode || document.body)
        .scrollTop;
  };

  useEffect(() => {
    window.addEventListener("scroll", scroll);

    return () => {
      window.removeEventListener("scroll", () => {});
    };
  }, []);

  return scrollHeight.current;
};

export default useScroll;

the value is not updating here.

but if I use useState here , it works. but that causes tremendous amount of component re-rendering. can you have any idea , how its happening?

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Arijit
  • 114
  • 1
  • 7
  • 3
    It *is* updating, but not causing an update of whatever depends on the ref, since when the value updates inside the hook, it isn't updating the value returned by the hook. – kelsny Nov 01 '22 at 15:02
  • Simple fix: throttle the callback: `scroll` – Shivam Jha Nov 01 '22 at 15:11

2 Answers2

1
import { useRef } from 'react';
import throttle from 'lodash.throttle';

/**
 * Hook to return the throttled function
 * @param fn function to throttl
 * @param delay throttl delay
 */
const useThrottle = (fn, delay = 500) => {
  // https://stackoverflow.com/a/64856090/11667949
  const throttledFn = useRef(throttle(fn, delay)).current;

  return throttledFn;
};

export default useThrottle;

then, in your custom hook:

const scroll = () => {
    scrollHeight.current =
      window.pageYOffset ||
      (document.documentElement || document.body.parentNode || document.body)
        .scrollTop;
  };

const throttledScroll = useThrottle(scroll)

Also, I like to point out that you are not clearing your effect. You should be:

useEffect(() => {
    window.addEventListener("scroll", throttledScroll);

    return () => {
      window.removeEventListener("scroll", throttledScroll); // remove Listener
    };
  }, [throttledScroll]); // this will never change, but it is good to add it here. (We've also cleaned up effect)

Shivam Jha
  • 3,160
  • 3
  • 22
  • 36
1

Since the hook won't rerender you will only get the return value once. What you can do, is to create a useRef-const in the useScroll hook. The useScroll hook returns the reference of the useRef-const when the hook gets mounted. Because it's a reference you can write the changes in the useScroll hook to the useRef-const and read it's newest value in a component which implemented the hook. To reduce multiple event listeners you should implement the hook once in the parent component and pass the useRef-const reference to the child components. I made an example for you.

The hook:

import { useCallback, useEffect, useRef } from "react";

export const useScroll = () => {
    const userScrollHeight = useRef(0);

    const scroll = useCallback(() => {
        userScrollHeight.current =
            window.pageYOffset ||
            (document.documentElement || document.body.parentNode || document.body)
                .scrollTop;
    }, []);

    useEffect(() => {
        window.addEventListener("scroll", scroll);

        return () => {
            window.removeEventListener("scroll", scroll);
        };
    }, []);

    return userScrollHeight;
};

The parent component:

import { SomeChild, SomeOtherChild } from "./SomeChildren";
import { useScroll } from "./ScrollHook";

const App = () => {
  const userScrollHeight = useScroll();

  return (
    <div>
      <SomeChild userScrollHeight={userScrollHeight} />
      <SomeOtherChild userScrollHeight={userScrollHeight} />
    </div>
  );
};

export default App;

The child components:

export const SomeChild = ({ userScrollHeight }) => {
    const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
        console.log("userScrollHeight from SomeChild", userScrollHeight.current);
    };

    return (
        <div style={{
            width: "100vw",
            height: "100vh",
            backgroundColor: "aqua"
        }}>
            <h1>SomeChild 1</h1>
            <button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
        </div>
    );
};

export const SomeOtherChild = ({ userScrollHeight }) => {
    const someButtonClickHandlerWhichPrintsUserScrollHeight = () => {
        console.log("userScrollHeight from SomeOtherChild", userScrollHeight.current);
    };

    return (
        <div style={{
            width: "100vw",
            height: "100vh",
            backgroundColor: "orange"
        }}>
            <h1>SomeOtherChild 1</h1>
            <button onClick={() => someButtonClickHandlerWhichPrintsUserScrollHeight()}>Console.log userScrollHeight</button>
        </div>
    );
};
Gipfeli
  • 197
  • 3
  • 8
  • Can you elaborate why you memoize the function here? – Arijit Nov 02 '22 at 04:25
  • Also this one doesn't work, plz create a sandbox demo, and share it – Arijit Nov 02 '22 at 04:55
  • I memoized the function, because of habit. You're unmounting the event listener which has been calling the scroll function. I'm not very sure what happens to the non memoized scroll function when the hook gets rerendered. Is the function reference the same? Since I have tested the callback version I'm always going for it. I haven't really tested the normal arrow function method. I will do a sandbox demo today if I have time. – Gipfeli Nov 02 '22 at 07:20
  • I created a sanbox: [https://codesandbox.io/s/optimistic-platform-v7qnli?file=/src/App.js](https://codesandbox.io/s/optimistic-platform-v7qnli?file=/src/App.js) – Gipfeli Nov 02 '22 at 07:28
  • Small update. If you want to update your state only for certain conditions then you can create an eventListener hook which takes a function as a parameter and calls the passed function. – Gipfeli Nov 24 '22 at 09:54