I'm trying to build an infinite scroll component in React (specifically using NextJS). I am having trouble with this feature because when I set a scroll event on the window, it doesn't have access to updated state. How can I write a scroll event that listens to any scrolling on the entire window that also has access to state like router query params?
Here's some code to see what I'm trying to do:
useEffect(() => {
window.addEventListener('scroll', handleScroll);
},[]);
const handleScroll = () => {
const el = infiniteScroll.current;
if (el) {
const rect = el.getBoundingClientRect();
const isVisible =
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <=
(window.innerWidth || document.documentElement.clientWidth);
if (isVisible && !isComplete && !isFetching) {
nextPage();
}
}
};
const nextPage = () => {
const params = router.query as any; // <------ these params here never update with state and are locked in to the the values they were at when the component mounted
params.page = params.page
? (parseInt((params as any).page) + 1).toString()
: '1';
router.replace(router, undefined, { scroll: false });
};
The issue is that the router value is locked at the place it was when the component mounted.
I've tried removing the empty array of dependencies for the useEffect at the top, but as you can imagine, this creates multiple scroll listeners and my events fire too many times. I've tried removing the eventListener before adding it every time, but it still fires too many times.
Every example I've found online seems to not need access to state variables, so they write code just like this and it works for them.
Any ideas how I can implement this?
I've tried to use the onScroll event, but it doesn't work unless you have a fixed height on the container so that you can use overflow-y: scroll.
You can use a ref to access and modify your state in the scope of the handleScroll function.
Here is how:
const yourRef = useRef('foo');
useEffect(() => {
const handleScroll = () => {
const value = yourRef.current;
if (value === 'foo') {
yourRef.current = 'bar'
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
I figured something out that works. Posting in case anyone else is having the same issue.
I created a custom hook called useScrollPosition that sets a listener on the window and updates the scroll position. It looks like this:
const useScrollPosition = () => {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const updatePosition = () => {
setScrollPosition(window.pageYOffset);
};
window.addEventListener('scroll', updatePosition);
updatePosition();
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return scrollPosition;
};
and using that in my component like this:
useEffect(() => {
handleScroll();
}, [scrollPosition]);
allows me to access the current state of the router
Related
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
}
I am trying to make a hook that returns the clientX and clientY values when the mouse moves on the screen. My hook looks like this -
useMouseMove hook
const useMouseMove = () => {
const [mouseData, setMouseData] = useState<[number, number]>([0, 0])
useEffect(() => {
const handleMouse = (e: MouseEvent) => {
setMouseData([e.clientX, e.clientY])
}
document.addEventListener("mousemove", handleMouse)
return () => {
document.removeEventListener("mousemove", handleMouse)
}
}, [])
return mouseData
}
And I'm using it in another component like so,
Usage in component
const SomeComponent = () => {
const mouseData = useMouseMoveLocation()
console.log("Rendered") // I want this to be rendered only once
useEffect(() => {
// I need to use the mouseData values here
console.log({ mouseData })
}, [mouseData])
return <>{/* Some child components */}</>
}
I need to use the mouseData values from the useMouseMove hook in the parent component (named SomeComponent in the above example) without re-rendering the entire component every time the mouse moves across the screen. Is there a correct way to do this to optimise for performance?
If you're not going to be rendering this component, then you can't use a useEffect. useEffect's only get run if your component renders. I think you'll need to run whatever code you have in mind in the mousemove callback:
const useMouseMove = (onMouseMove) => {
useEffect(() => {
document.addEventListener("mousemove", onMouseMove)
return () => {
document.removeEventListener("mousemove", onMouseMove)
}
}, [onMouseMove])
}
const SomeComponent = () => {
useMouseMove(e => {
// do something with e.clientX and e.clientY
});
}
I'm trying to do something with setTimeout on a switch controller but I don't know what is the problem and I get this error when the code is run, this in fact is a custom hook I use: Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
import React from 'react';
const useVisibility = () => {
const [visibility, setVisibility] = React.useState(true);
const [firstTime, setFirstTime] = React.useState(true);
let timeOutId;
const removeTimer = () => {
clearTimeout(timeOutId);
timeOutId = 0;
};
React.useEffect(
() => {
document.addEventListener('visibilitychange', (e) => {
if (document.hidden) {
switch (firstTime) {
case true:
setFirstTime(false)
timeOutId = setTimeout(() => {
setVisibility(false);
}, 0);
break;
default:
timeOutId = setTimeout(() => {
setVisibility('closed');
}, 0);
break;
}
} else if (document.isConnected) {
removeTimer();
}
});
},
[visibility]
);
return { visibility, setVisibility };
};
export default useVisibility;
And here is how I'm using it, and also calling a React function inside it:
{
visibility === 'closed' ? <> {cheatingPrevent()}
<Modal onClose={() => setVisibility(true)}
title="test"
text="test." /> </> : null
}
React.useEffect will add an event listener to document every time visibility changes as you have it in the dependency array. For each visibilitychange event, all the duplicate event listeners added will run.
The problem with this is you're calling setVisibility in useEffect callback which updates visibility which in return re-runs useEffect.
You don't need visibility in dependency array of useEffect hook. Pass empty array []
I have a header that I want to hide on scroll down and show on scroll up.
To do that, I saved the scrolling position as prevScrollPos in the state to compare it to the current scrolling position onscroll, and then update prevScrollPos to the current:
const [visible, setVisible] = React.useState(true);
const [prevScrollPos, setPrevScrollPos] = React.useState(window.pageYOffset);
const handleScroll = () => {
const scrollPos = window.pageYOffset;
const visible = scrollPos < prevScrollPos;
setVisible(visible);
setPrevScrollPos(scrollPos);
}
The problem is that, for some reason PrevScrollPos doesn't get updated.
Pen: https://codepen.io/moaaz_bs/pen/jgGRoj?editors=0110
You need to modify your useEffect function:
React.useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
});
Basically you don't have access to prevScrollPos in your handler, therefore prevScrollPos inside the listener will always return 0. To solve this, the dependency array should not be present.
-> + Do not forget to remove the event listener after adding it. :-)
Can you try this:
const [visible, setVisible] = React.useState(true);
const [prevScrollPos, setPrevScrollPos] = React.useState(window.pageYOffset);
const handleScroll = () => {
const scrollPos = window.pageYOffset;
const visible_new = scrollPos < prevScrollPos;
setVisible(visible_new);
setPrevScrollPos(scrollPos);
}
I create simple custom hook that save the screen height and width .
The problem is that I want to re-render(update state) only if some condition in my state is happened and not in every resize event..
I try first with simple implementation :
const useScreenDimensions = () => {
const [height, setHeight] = useState(window.innerWidth);
const [width, setWidth] = useState(window.innerHeight);
const [sizeGroup, setSizeGroup]useState(getSizeGroup(window.innerWidth));
useEffect(() => {
const updateDimensions = () => {
if (getSizeGroup() !== sizeGroup) {
setSizeGroup(getSizeGroup(width));
setHeight(window.innerHeight);
setWidth(window.innerWidth);
}
};
window.addEventListener('resize', updateDimensions);
return () => window.removeEventListener('resize', updateDimensions);
}, [sizeGroup, width]);
return { height, width };
}
The problem with this approach is that the effect calls every time , I want that the effect will call just once without dependencies (sizeGroup, width) because I don't want to register the event every time there is a change in screen width/size group(window.addEventListener).
So, I try with this approach with UseCallBack , but also here my 'useEffect' function called many times every time there is any change in the state..
//useState same as before..
const updateDimensions = useCallback(() => {
if (getSizeGroup(window.innerWidth) !== sizeGroup) {
setSizeGroup(getSizeGroup(width));
setHeight(window.innerHeight);
setWidth(window.innerWidth);
}
}, [sizeGroup, width]);
useEffect(() => {
window.addEventListener('resize', updateDimensions);
return () => window.removeEventListener('resize', updateDimensions);
}, [updateDimensions]);
....
return { height, width };
The question is what the correct and effective way for my purposes? I want just "register" the event once, and update the my state only when my state variable is true and not every time the width or something else get updated..
I know that when you set empty array as second argument to 'UseEffect' it's run only once but in my case I want that the register of my event listener run once and on resize I will update the state only if some condition is true
Thanks a lot.
use 2 different useEffect
first one for register event.So below code will run at the time of componentDidMount.
useEffect(() => {
window.addEventListener('resize', updateDimensions);
}, []);
second useEffect to run based on state change.
useEffect(() => {
updateDimensions();
return () => window.removeEventListener('resize', updateDimensions);
}, [sizeGroup, width])
const updateDimensions = useCallback(() => {
setSizeGroup(getSizeGroup(width));
setHeight(window.innerHeight);
setWidth(window.innerWidth);
}
I'm not sure useCallback function need to use or not. And I've not tested this code.