I am working on a hook that keeps track of the maximum depth a user scrolls down a page. I would like to make use of this value inside the useEffect's cleanup function so that I may log the value when the component is unmounted.
The hook appears to be working, but the implementation inside the Home component is not. It either logs continuously while scrolling if I populate the dependency array, or it logs a single 0 when the component unmounts.
I would like for it to log the single correct value when the component unmounts.
Thanks.
import { useEffect, useRef } from 'react';
export const useTrackScrollDepth = () => {
const scrollDepthRef = useRef(0);
const trackScrollDepth = () => {
const scrolledValue = window.scrollY;
scrollDepthRef.current = Math.max(scrolledValue, scrollDepthRef.current);
};
useEffect(() => {
const handleScrollTracking = () => {
trackScrollDepth();
};
document.addEventListener('scroll', handleScrollTracking);
return () => {
document.removeEventListener('scroll', handleScrollTracking);
};
}, []);
return scrollDepthRef.current;
};
Then in my component, I make use of the hook as so:
import React, { useEffect } from 'react';
import { useTrackScrollDepth } from 'hooks/useTrackScrollDepth';
export function Home() {
const maxScrollDepth = useTrackScrollDepth();
useEffect(
() => () => {
console.log(`Max scroll depth: ${maxScrollDepth}`);
},
[],
);
return (
<div>Home placeholder</div>
);
}
When you use return scrollDepthRef.current;
in your hook, it returns the current number on each render. Since the hook doesn't cause re-render, the number stays at 0.
When you add that to the dependencies of useEffect
, the scroll depth changed, and something causes a re-render, useEffect
cleanup is triggered, and the previous value is displayed.
Instead return the reference itself (return scrollDepthRef;
), and use it in the useEffect
to get the current
value:
const { useEffect, useRef, useCallback, useState } = React;
const useTrackScrollDepth = () => {
const scrollDepthRef = useRef(0);
useEffect(() => {
const handleScrollTracking = () => {
const scrolledValue = window.scrollY;
scrollDepthRef.current = Math.max(scrolledValue, scrollDepthRef.current);
};
document.addEventListener('scroll', handleScrollTracking);
return () => {
document.removeEventListener('scroll', handleScrollTracking);
};
}, []);
return scrollDepthRef; // return the reference and not the current value
};
function Home() {
const scrollDepthRef = useTrackScrollDepth();
useEffect(() => () => {
console.log(`Max scroll depth: ${scrollDepthRef.current}`); // get the current value
}, [scrollDepthRef]); // use the ref as dependency
return (
<div className="home">Home placeholder</div>
);
}
const Demo = () => {
const [show, setShow] = useState(true);
const toggle = () => setShow(s => !s);
return (
<div>
<button onClick={toggle}>Toggle</button>
{show && <Home />}
</div>
);
};
ReactDOM
.createRoot(root)
.render(<Demo />);
button {
position: fixed;
}
.home {
height: 200vh;
background: red;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>
Another option is to return a memoized function from the hook, and call it on unmount:
const { useEffect, useRef, useCallback, useState } = React;
const useTrackScrollDepth = () => {
const scrollDepthRef = useRef(0);
useEffect(() => {
const handleScrollTracking = () => {
const scrolledValue = window.scrollY;
scrollDepthRef.current = Math.max(scrolledValue, scrollDepthRef.current);
};
document.addEventListener('scroll', handleScrollTracking);
return () => {
document.removeEventListener('scroll', handleScrollTracking);
};
}, []);
return useCallback(() => scrollDepthRef.current, []); // return a memoized function that returns the current value
};
function Home() {
const getMaxScrollDepth = useTrackScrollDepth();
useEffect(() => () => {
console.log(`Max scroll depth: ${getMaxScrollDepth()}`); // get the current value
}, [getMaxScrollDepth]);
return (
<div className="home">Home placeholder</div>
);
}
const Demo = () => {
const [show, setShow] = useState(true);
const toggle = () => setShow(s => !s);
return (
<div>
<button onClick={toggle}>Toggle</button>
{show && <Home />}
</div>
);
};
ReactDOM
.createRoot(root)
.render(<Demo />);
button {
position: fixed;
}
.home {
height: 200vh;
background: red;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>