How can I properly debounce a wheel event in ReactJS? - reactjs

Made a codesandbox of the issue : https://codesandbox.io/s/serene-rosalind-fcitpd?file=/src/Photo.tsx
The real problem comes when you change scroll direction, the index photo will glitch a bit ...
I have a photo gallery:https://virgile-hasselmann.vercel.app/photos. I want to switch the photos when the user scrolls up or down. To do so I thought I'd use wheelEvent and the e.deltaY. It kinda works but I had to add a useDebounce function because if not it would glitch the gallery. Here you can see the custom hook :
function useDebounce<T>(value: T, delay?: number) {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
const setDebounce = (newValue: T) => {
setTimeout(() => setDebouncedValue(newValue), delay || 500);
};
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return { debouncedValue, setDebounce };
}
In the Gallery component, here is how I've implemented the wheel event and the debouncing :
// Init the data to display with the photos of the first category
const [photoIdx, setPhotoIdx] = useState(0);
const { debouncedValue, setDebounce } = useDebounce(photoIdx, 1000);
const setDebouncedIdx = (value: number) => {
setDebounce(value);
setPhotoIdx(debouncedValue);
};
const handleWheel = (e: WheelEvent<HTMLDivElement>) => {
if (e.deltaY > 0) {
if (photoIdx < dataSelected!.length - 1) {
setDebouncedIdx(photoIdx + 1);
} else {
setDebouncedIdx(0);
}
} else {
if (photoIdx > 0) {
setDebouncedIdx(photoIdx - 1);
} else {
setDebouncedIdx(dataSelected!.length - 1);
}
}
};
But the result does not satisfy me the least: it's glitchy and not really responding the user's input + if you try to go back by scrolling back, it will first show you the next photo before going back to it. Hard to explain word but it's quite clear if you look at the index at the bottom left corner.
Perhaps there is a better way of implementing it. The problem is unclear to me if anyone could enlighten me that would much appreciated :)

I think you can use an eventListener to detect if the user is scrolling. You can detect the direction of the scrolling too. So if the user scrolls down then you replace the image with the next one. Or scroll up then replace it with the previous one.
The threshold is used to determine how much you want to scroll before taking an action.
useEffect(() => {
//Scroll listener to determine the scroll direction
const threshold = 0;
let lastScrollY = window.scrollY;
let scrollDir = "";
const updateScrollDir = () => {
const scrollY = window.scrollY;
if (Math.abs(scrollY - lastScrollY) < threshold) {
return;
}
scrollDir = (scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
if (scrollDir == "scrolling up") {
//Do something to display the previous image
}
if (scrollDir == "scrolling down") {
//Do something to display the next image
}
};
const onScroll = () => {
window.requestAnimationFrame(updateScrollDir);
};
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
}
},

Related

Smooth animation of the search bar and the dropdown list along with it

I'm trying to achieve smooth animation of searchbar (MUI Autocomplete). This should work only on Smartphones (Screen < 600px).
Here is an example (it is very buggy and open it on smartphone to see the animation): https://react-zxuspr-gjq5w8.stackblitz.io/
And here is my implementation, but I've a few problems with that:
The interval does not reset on dropdown close.
The React.useEffect() dependency is set to searchActive, which is changed dynamically.
I tried calling the callback function of React.useState(), but since the component is not destroyed, I am not sure if it makes sense.
The width of dropdown, which is also changed in the setInterval() function, is not smooth at all.
https://stackblitz.com/edit/react-zxuspr-gjq5w8?file=demo.js
Here is part of the component where the logic is implemented:
export function PrimarySearchAppBar() {
const [searchActive, setSearchActive] = React.useState(null);
const [acPaperWidth, setAcPaperWidth] = React.useState(null);
const [acPaperTransX, setAcPaperTransX] = React.useState(0);
const AcRef = React.useRef(null);
const isMobile = useMediaQuery(useTheme().breakpoints.down('sm'));
const options = top100Films.map((option) => {
const group = option.group.toUpperCase();
return {
firstLetter: group,
...option,
};
});
React.useLayoutEffect(() => {
if (AcRef.current) {
setAcPaperWidth(AcRef.current.offsetWidth);
}
console.log(acPaperWidth);
}, [AcRef]);
let interval;
React.useEffect(() => {
if (searchActive) {
if (acPaperTransX <= 39) {
interval = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
setAcPaperWidth((acPaperTransX) => acPaperTransX + 1);
if (acPaperTransX >= 38) {
clearInterval(interval);
}
console.log(acPaperTransX);
}, 10);
}
} else {
setAcPaperTransX(0);
clearInterval(interval);
}
}, [searchActive]);
return (
<>Hello World</>
);
}
The problem was that the interval variable was defined outside the React.useEffect() hook, so its value was not preserved on re-renders.
I was able to fix that by using React.useRef():
const intervalRef = React.useRef();
const acPaperTransXRef = React.useRef(acPaperTransX);
React.useEffect(() => {
if (searchActive) {
if (acPaperTransXRef.current <= 39) {
intervalRef.current = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
acPaperTransXRef.current += 0.1;
if (acPaperTransXRef.current >= 38) {
clearInterval(intervalRef.current);
acPaperTransXRef.current = 0;
}
console.log(acPaperTransXRef.current);
}, 2);
}
} else {
setAcPaperTransX(0);
acPaperTransXRef.current = 0;
clearInterval(intervalRef.current);
}
return () => clearInterval(intervalRef.current);
}, [searchActive]);

Framer-motion Animation With Typescript: ScrollY doesn't find .current

I'm trying to create an animation that hides the menu when the user scrolls down and when the user returns to the top the menu appears.
Everything works, but the problem comes when I try to give to the scrollY property that gives the useScroll hook a type.
This is the code that I could not find the way to implement the type.
const { scrollY } = useScroll();
const [isScrolling, setScrolling] = useState<boolean>(false);
const handleScroll = () => {
if (scrollY?.current < scrollY?.prev) setScrolling(false);
if (scrollY?.current > 100 && scrollY?.current > scrollY?.prev) setScrolling(true);
};
useEffect(() => {
return scrollY.onChange(() => handleScroll());
});
I found the solution!
Reading the documentation framer, I saw that they updated the way to get the value of the previous and current scroll.
scrollY.get() -> Current
scrollY.getPrevious() -> Previous
const { scrollY } = useScroll();
const [isScrolling, setScrolling] = useState<boolean>(false);
const handleScroll = () => {
if (scrollY.get() < scrollY.getPrevious()) setScrolling(false);
if (scrollY.get() > 100 && scrollY.get() > scrollY.getPrevious()) setScrolling(true);
};
useEffect(() => {
return scrollY.onChange(() => handleScroll());
});

useScrollDirection Hook makes slide in button to flicker

It is quite simple yet annoying to go around.
While scrolling down on mobile device, nothing should be happening. But the moment you scroll upwards i am using a Custom Hook to recognise direction and then trigger transition for button to slide in.
However, as the screen is still moving, my Hook is still re-rendering and makes my button flicker. It does slide in if the scrolling stopps.
I just cant get around this to make it slide in smoothly.
Here is the Hook.
import { useEffect, useState } from "react"
export const SCROLL_UP = "up"
export const SCROLL_DOWN = "down"
export const useScrollDirection = (
initialDirection = SCROLL_DOWN,
thresholdPixels = 20
) => {
const [scrollDir, setScrollDir] = useState(initialDirection)
useEffect(() => {
const threshold = thresholdPixels || 0
let lastScrollY = window.pageYOffset
let ticking = false
const updateScrollDir = () => {
const scrollY = window.pageYOffset
if (Math.abs(scrollY - lastScrollY) < threshold) {
// We haven't exceeded the threshold
ticking = false
return
}
setScrollDir(scrollY > lastScrollY ? SCROLL_DOWN : SCROLL_UP)
lastScrollY = scrollY > 0 ? scrollY : 0
ticking = false
}
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir)
ticking = true
}
}
window.addEventListener("scroll", onScroll)
return () => window.removeEventListener("scroll", onScroll)
}, [initialDirection, thresholdPixels])
return scrollDir
}

React useScroll Hook keep track of previousScrollTop

So I've followed this:
https://www.pinkdroids.com/blog/moving-header-react-hooks-context/
And managed to get it working on one app.
I'm not replicating what I did in another app and it's not working.
I've got as far as working out that my previousScrollTop and currentScrollTop are always the same – therefore no direction is being added
But why are they the same!!!
I've gone as far as copy and pasting the code so no issues there.
Could this be a problem with memo?
with my lodash import?
Not getting any errors.
UPDATE*
The problem is either with useMemo, or useRef in my useScroll hook.
const getScrollPosition = () => {
if (typeof window === 'undefined') {
return 0
}
return (
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop ||
0
)
}
export const useScroll = (timeout = 250) => {
const defaultScrollTop = useMemo(() => getScrollPosition(), [])
const previousScrollTop = useRef(defaultScrollTop)
const [currentScrollTop, setCurrentScrollTop] = useState(defaultScrollTop)
console.log(currentScrollTop)
useEffect(() => {
const handleDocumentScroll = () => {
const scrollTop = getScrollPosition()
setCurrentScrollTop(scrollTop)
console.log(currentScrollTop)
previousScrollTop.current = scrollTop
console.log(previousScrollTop.current)
}
const handleDocumentScrollThrottled = throttle(handleDocumentScroll, timeout)
document.addEventListener('scroll', handleDocumentScrollThrottled)
return () => {
document.removeEventListener('scroll', handleDocumentScrollThrottled)
}
}, [timeout])
return {
scrollTop: currentScrollTop,
previousScrollTop: previousScrollTop.current,
time: timeout,
}
}
UPDATE*
I've managed to work a fix with a setTimeout inside my handleDocumentScroll... Should delay between the values...
But still unsure why this worked before as it was and not now....
const handleDocumentScroll = () => {
const scrollTop = getScrollPosition()
setCurrentScrollTop(scrollTop)
setTimeout(function () {
previousScrollTop.current = scrollTop
}, 200); // update previous 200ms after event fires
}

Modal trigger - mouse position

I have a question about React. How can I trigger a certain function in the useEffect hook, depending on the position of the cursor?
I need to trigger a function that will open a popup in two cases - one thing is to open a modal after a certain time (which I succeeded with a timeout method), but the other case is to trigger the modal opening function once the mouse is at the very top of the website right in the middle. Thanks for the help.
For now I have that, but I'm struggling with the mouse position part:
function App(e) {
const [modalOpened, setModalOpened] = useState(false);
console.log(e)
useEffect(() => {
const timeout = setTimeout(() => {
setModalOpened(true);
}, 30000);
return () => clearTimeout(timeout);
}, []);
const handleCloseModal = () => setModalOpened(false);
You could use a custom hook that checks the conditions you want.
I'm sure this could be cleaner, but this hook works.
export const useMouseTopCenter = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isTopCenter, setIsTopCenter] = useState(false);
const width = window.innerWidth;
// Tracks mouse position
useEffect(() => {
const setFromEvent = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", setFromEvent);
return () => {
window.removeEventListener("mousemove", setFromEvent);
};
}, []);
// Tracks whether mouse position is in the top 100px and middle third of the screen.
useEffect(() => {
const centerLeft = width / 3;
const centerRight = (width / 3) * 2;
if (
position.x > centerLeft &&
position.x < centerRight &&
position.y < 100
) {
setIsTopCenter(true);
} else {
setIsTopCenter(false);
}
}, [position, width]);
return isTopCenter;
};
Then you would simply add const isCenterTop = useMouseTopCenter(); to your app component.
Checkout this code sandbox, which is just a variation of this custom hook.

Resources