-
-
Save joshuacerbito/ea318a6a7ca4336e9fadb9ae5bbb87f4 to your computer and use it in GitHub Desktop.
/** | |
* useScroll React custom hook | |
* Usage: | |
* const { scrollX, scrollY, scrollDirection } = useScroll(); | |
*/ | |
import { useState, useEffect } from "react"; | |
export function useScroll() { | |
const [lastScrollTop, setLastScrollTop] = useState(0); | |
const [bodyOffset, setBodyOffset] = useState( | |
document.body.getBoundingClientRect() | |
); | |
const [scrollY, setScrollY] = useState(bodyOffset.top); | |
const [scrollX, setScrollX] = useState(bodyOffset.left); | |
const [scrollDirection, setScrollDirection] = useState(); | |
const listener = e => { | |
setBodyOffset(document.body.getBoundingClientRect()); | |
setScrollY(-bodyOffset.top); | |
setScrollX(bodyOffset.left); | |
setScrollDirection(lastScrollTop > -bodyOffset.top ? "down" : "up"); | |
setLastScrollTop(-bodyOffset.top); | |
}; | |
useEffect(() => { | |
window.addEventListener("scroll", listener); | |
return () => { | |
window.removeEventListener("scroll", listener); | |
}; | |
}); | |
return { | |
scrollY, | |
scrollX, | |
scrollDirection | |
}; | |
} |
Thank you so much for this hook!
Okay, sorry to double post.... if Any Gatsby or React SSR users come across this, of course, your gonna run into a document is undefined error.
For gatsby build time I have solved it by using a ternary when using the document element:
import { useState, useEffect } from "react";
export function useScroll() {
const [lastScrollTop, setLastScrollTop] = useState(0);
const [bodyOffset, setBodyOffset] = useState(
typeof window === "undefined" || !window.document ? 0 : document.body.getBoundingClientRect()
);
const [scrollY, setScrollY] = useState(bodyOffset.top);
const [scrollX, setScrollX] = useState(bodyOffset.left);
const [scrollDirection, setScrollDirection] = useState();
const listener = e => {
setBodyOffset(typeof window === "undefined" || !window.document ? 0 : document.body.getBoundingClientRect());
setScrollY(-bodyOffset.top);
setScrollX(bodyOffset.left);
setScrollDirection(lastScrollTop > -bodyOffset.top ? "down" : "up");
setLastScrollTop(-bodyOffset.top);
};
useEffect(() => {
window.addEventListener("scroll", listener);
return () => {
window.removeEventListener("scroll", listener);
};
});
return {
scrollY,
scrollX,
scrollDirection
};
}
If your debouncing the listener, I think you are removing it wrong. you should correctly remove it by doing something like:
useEffect(() => {
const debounceWrapper = debounce(listener, 300)
window.addEventListener('scroll', debounceWrapper)
return () => {
window.removeEventListener('scroll', debounceWrapper)
}
}, [])
otherwise you'll notice that if u do a console log in the listener and change pages it will still be firing.
This hook is extremely useful and worked better than most libraries for such. I did some changes on the original one and also converted it to Typescript. Be free to use it or make improvements:
https://gist.github.com/gusfune/5ee7d6815db966ab16d88dda7cf414da
/**
* useScroll React custom hook
* Usage:
* const { scrollX, scrollY, scrollDirection } = useScroll();
* Original Source: https://gist.github.com/joshuacerbito/ea318a6a7ca4336e9fadb9ae5bbb87f4
*/
import { useState, useEffect } from "react"
type SSRRect = {
bottom: number
height: number
left: number
right: number
top: number
width: number
x: number
y: number
}
const EmptySSRRect: SSRRect = {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
x: 0,
y: 0,
}
const useScroll = () => {
const [lastScrollTop, setLastScrollTop] = useState<number>(0)
const [bodyOffset, setBodyOffset] = useState<DOMRect | SSRRect>(
typeof window === "undefined" || !window.document
? EmptySSRRect
: document.body.getBoundingClientRect()
)
const [scrollY, setScrollY] = useState<number>(bodyOffset.top)
const [scrollX, setScrollX] = useState<number>(bodyOffset.left)
const [scrollDirection, setScrollDirection] = useState<
"down" | "up" | undefined
>()
const listener = () => {
setBodyOffset(
typeof window === "undefined" || !window.document
? EmptySSRRect
: document.body.getBoundingClientRect()
)
setScrollY(-bodyOffset.top)
setScrollX(bodyOffset.left)
setScrollDirection(lastScrollTop > -bodyOffset.top ? "down" : "up")
setLastScrollTop(-bodyOffset.top)
}
useEffect(() => {
window.addEventListener("scroll", listener)
return () => {
window.removeEventListener("scroll", listener)
}
})
return {
scrollY,
scrollX,
scrollDirection,
}
}
export { useScroll }
Can you pls add a license? Because if you don't add it, unfortunately, no one does not have a right to use it.
Here is my version that takes inspiration from a combination of the versions posted here but I've also included optional callbacks that can be added to the hook when using it.
https://gist.github.com/csandman/289787f26ae14566963ba611bf999c1f
// inspired by:
// https://gist.github.com/joshuacerbito/ea318a6a7ca4336e9fadb9ae5bbb87f4
import { useEffect, useState } from 'react';
const isValidFunction = (func) => {
return func && typeof func === 'function';
};
export default function useScroll({ onScroll, onScrollUp, onScrollDown }) {
const [scroll, setScroll] = useState(
typeof window === 'undefined' || !window.document
? { x: 0, y: 0, direction: '' }
: {
x: document.body.getBoundingClientRect().left,
y: -document.body.getBoundingClientRect().top,
direction: '',
}
);
useEffect(() => {
const handleScroll = () => {
setScroll((prevScroll) => {
const rect =
typeof window === 'undefined' || !window.document
? { left: 0, top: 0 }
: document.body.getBoundingClientRect();
const x = rect.left;
const y = -rect.top;
const direction = prevScroll.y > y ? 'up' : 'down';
const newScroll = { x, y, direction };
if (isValidFunction(onScroll)) {
onScroll(newScroll);
}
if (direction === 'up' && isValidFunction(onScrollUp)) {
onScrollUp(newScroll);
}
if (direction === 'down' && isValidFunction(onScrollDown)) {
onScrollDown(newScroll);
}
return newScroll;
});
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [onScroll, onScrollDown, onScrollUp]);
return scroll;
}
import {useEffect, useState} from 'react';
export const DIRECTION = {
down: 'DOWN',
up: 'UP',
unset: 'UNSET',
};
const getDocumentBoundingClientRect = (documentElement) =>
typeof documentElement.getBoundingClientRect === 'function' ?
documentElement.getBoundingClientRect() :
{
top: 0,
left: 0,
};
const getDocumentElement = (isServer) =>
!isServer ?
document.documentElement
: {
scrollHeight: 0,
scrollWidth: 0,
getBoundingClientRect: getDocumentBoundingClientRect,
};
const getWindowSize = (isServer) => ({
innerHeight: !isServer ? window.innerHeight : 0,
innerWidth: !isServer ? window.innerWidth : 0,
});
const createScrollState = (lastScrollTop) => {
const isServer = !process.browser;
const documentElement = getDocumentElement(isServer);
const bodyBoundingRect = documentElement.getBoundingClientRect();
const windowSize = getWindowSize(isServer);
const scrollY = bodyBoundingRect.top;
const scrollX = bodyBoundingRect.left;
const scrollYMax = documentElement.scrollHeight - windowSize.innerHeight;
const scrollXMax = documentElement.scrollWidth - windowSize.innerWidth;
const scrollDirection = lastScrollTop > bodyBoundingRect.top ? DIRECTION.down : DIRECTION.up;
return {
scrollY,
scrollX,
scrollDirection,
scrollYMax,
scrollXMax,
}
};
const useWindowScroll = () => {
const [state, setState] = useState(createScrollState(0));
useEffect(() => {
const listener = () =>
setState(previousState =>
createScrollState(previousState.scrollY)
);
window.addEventListener('scroll', listener);
return () => {
window.removeEventListener('scroll', listener);
};
}, []);
return state;
};
export default useWindowScroll;
Anyone tried any of above code for infinite scroll ?
import React, {useRef, useEffect} from 'react';
const useComponentScrollHook = (callBack) => {
const ref = useRef(null);
useEffect(() => {
if (ref.current && callBack) {
ref.current.addEventListener('scroll', callBack);
}
return () => {
if (ref.current && callBack) {
ref.current.removeEventListener('scroll', callBack);
}
};
}, [ref, callBack]);
return ref;
};
export default useComponentScrollHook;
const scrollCallback = useCallback((e) => {
const maxScroll = e.target.scrollHeight - e.target.offsetHeight;
const scrollTop = e.target.scrollTop;
const difference = maxScroll - scrollTop;
if (difference <= 0 && !finished) {
fetchData();
}
}, [finished, fetchData])
const ref = useComponentScrollHook(scrollCallback);```
Only register 'scroll' event one time:
import { useState, useEffect, useCallback } from 'react'
export const useScroll = () => {
const [state, setState] = useState({
lastScrollTop: 0,
bodyOffset: document.body.getBoundingClientRect(),
scrollY: document.body.getBoundingClientRect().top,
scrollX: document.body.getBoundingClientRect().left,
scrollDirection: '', // down, up
})
const handleScrollEvent = useCallback((e) => {
setState((prevState) => {
const prevLastScrollTop = prevState.lastScrollTop
const bodyOffset = document.body.getBoundingClientRect()
return {
setBodyOffset: bodyOffset,
scrollY: -bodyOffset.top,
scrollX: bodyOffset.left,
scrollDirection: prevLastScrollTop > -bodyOffset.top ? 'down' : 'up',
lastScrollTop: -bodyOffset.top,
}
})
}, [])
useEffect(() => {
const scrollListener = (e) => {
handleScrollEvent(e)
}
window.addEventListener('scroll', scrollListener)
return () => {
window.removeEventListener('scroll', scrollListener)
}
}, [handleScrollEvent])
return {
scrollY: state.scrollY,
scrollX: state.scrollX,
scrollDirection: state.scrollDirection,
}
}
export default useScroll
Hi,
Really nice what you all did above, my question is the first post made by @joshuacerbito is the updated version after all comments made by the community or anyone came with let's say a "better solution".
Thanks 👍
That helped. Thank you.