Photo by Ćmile Perron on Unsplash
At Foodnome, I wanted to create a fixed badge that appears if the user scrolls to the top of the page and disappears as the user scrolls to the bottom of the page.
To accomplish this, I knew exactly what to do (or that is what I thought):
- use a
useState
hook to keep the old scroll position in local state. - add a
useEffect
to add an event listener to thewindow
to listen to changes in the scroll position, and fire a callback function to update the internal scroll position state. Return a callback to remove the event listener when the component unmount. - return the difference between the new scroll position (
window.pageYOffset
) and the old position (oldScrollPos
). If the difference is positive, then the user is scrolling to the bottom of the page. Otherwise, the user is scrolling back to the top.
function useScrollDirection() {
/*
return value:
true => user is scrolling to the bottom of the page.
false => user is scrolling to the top of the page.
*/
const [oldScrollPos, setOldScrollPos] = React.useState(0)
React.useEffect(() => {
function onScroll() {
setOldScrollPos(window.pageYOffset)
}
window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
}, [])
// current scroll position minus the old scroll position saved in state.
const difference = window.pageYOffset - oldScrollPos
return difference > 0
}
Then in my component using this custom hook:
function EventDetail(props) {
// use the hook to listen to changes in the scroll position
const scrollUp = useScrollDirection()
return (
<div>
{/* do something here to react to changes in the scroll direction */}
</div>
)
}
This didnāt work š. When I added a console.log
of the difference
into my custom hook, I quickly realized that the old scroll state saved in oldScrollPos
was exactly the same as the new scroll state window.pageYOffset
(and hence, difference
was equal to 0
.) So, I needed to figure out how to hold onto this old scroll state and I couldnāt figure it out. At some point, I remembered a blog post by Dan Abramov about keeping some mutable state around using refs. Iāve read about using refs for referencing and focusing input fields and I used to use refs when dealing with setInterval
in a class
based component. For example,
class MyComponent extends React.Component {
state = { count: 0 }
componentDidMount() {
this.interval = setInterval(
() =>
this.setState(({ count }) => ({
count: count + 1,
})),
1000
)
}
componentWillUnmount() {
clearInterval(this.interval)
}
render() {
return <p>{this.state.count}</p>
}
}
Anyway, this is exactly what I needed to complete my custom hook! š£
function useScrollDirection() {
/*
return value:
true => user is scrolling to the bottom of the page.
false => user is scrolling to the top of the page.
*/
// save the new scroll position in state
const [scrollPos, setScrollPos] = React.useState(0)
// useRef Hook to save the old scroll state.
const oldScrollPos = React.useRef(0)
React.useEffect(() => {
function onScroll() {
setScrollPos(window.pageYOffset)
// save the old scroll position in the ref
oldScrollPos.current = window.pageYOffset
}
window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
}, [])
// current scroll position minus the old scroll position saved in state.
const difference = scrollPos - oldScrollPos.current
return difference > 0
}
I am still not 100% sure if this is the best way of doing this but, I am proud I found a solution to the feature I was trying to implement using the useRef
hook!
Special thanks to @donavon for the valuable feedback!