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);
}
Related
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());
});
I want to detect which of the argument props have changed inside my use effect. How can I achieve this? I need something like this:
const myComponent = () => {
...
useEffect(() => {
if (prop1 === isTheOneThatChangedAndCusedTheTrigger){
doSomething(prop1);
}else{
doSomething(prop2);
}
},[prop1, prop2]);
};
export function myComponent;
While you can use something like usePrevious to retain a reference to the last value a particular state contained, and then compare it to the current value in state inside the effect hook...
const usePrevious = (state) => {
const ref = useRef();
useEffect(() => {
ref.current = state;
}, [value]);
return ref.current;
}
const myComponent = () => {
const [prop1, setProp1] = useState();
const prevProp1 = usePrevious(prop1);
const [prop2, setProp2] = useState();
const prevProp2 = usePrevious(prop2);
useEffect(() => {
if (prop1 !== prevProp1){
doSomething(prop1);
}
// Both may have changed at the same time - so you might not want `else`
if (prop2 !== prevProp2) {
doSomething(prop2);
}
},[prop1, prop2]);
};
It's somewhat ugly. Consider if there's a better way to structure your code so that this isn't needed - such as by using separate effects.
useEffect(() => {
doSomething(prop1);
}, [prop1]);
useEffect(() => {
doSomething(prop2);
}, [prop2]);
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
I'm getting the selected checkbox name into an array. Then, I want to use it later for some calculations. but when I try to get the values it's giving the wrong values. I want to allow users to select the ticket count by using the +/- buttons. following is my code
Code
Getting the selected seat from the checkboxes
const [seatCount, setSeatCount] =React.useState("");
const [open, setOpen] = React.useState(false);
const [counter, setCounter] = React.useState(0);
var seatsSelected = []
const handleChecked = async (e) => {
let isChecked = e.target.checked;
if(isChecked){
await seatsSelected.push(e.target.name)
} else {
seatsSelected = seatsSelected.filter((name) => e.target.name !== name);
}
console.log(seatsSelected);
}
calculations happening on a dialog
const handleClickOpen = () => { //open the dialog
setOpen(true);
console.log(seatsSelected);
const seatTotal = seatsSelected.length
console.log(seatTotal)
setSeatCount(seatTotal)
console.log(seatCount)
};
const handleClose = () => { //close the dialog
setOpen(false);
};
const handleIncrement = () =>{ //increse the count
if(counter < seatsSelected.length){
setCounter(counter + 1)
} else {
setCounter(counter)
}
}
const handleDecrement = () =>{ //decrese the count
if(counter >= seatsSelected.length){
setCounter(counter - 1)
} else {
setCounter(counter)
}
}
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(seatCount);
}, [seatCount);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
I tried adding the condition on mouseenter and mouseleave however the modal is not working but when I tried to create a button onClick={() => {openModal();}} the modal will show up. Can you please tell me what's wrong on my code and which part.
const openModal = event => {
if (event) event.preventDefault();
setShowModal(true);
};
const closeModal = event => {
if (event) event.preventDefault();
setShowModal(false);
};
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
return () => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
};
}, [ref]);
return [ref, hovered];
}
const [ref, hovered] = useHover();
<div className="hover-me" ref={ref}>hover me</div>
{hovered && (
<Modal active={showModal} closeModal={closeModal} className="dropzone-modal">
<div>content here</div>
</Modal>
)}
building on Drew Reese's answer, you can cache the node reference inside the useEffect closure itself, and it simplifies things a bit. You can read more about closures in this stackoverflow thread.
const useHover = () => {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
const el = ref.current; // cache external ref value for cleanup use
if (el) {
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventLisener("mouseenter", enter);
el.removeEventLisener("mouseleave", leave);
};
}
}, []);
return [ref, hovered];
};
I almost gave up and passed on this but it was an interesting problem.
Issues:
The first main issue is with the useEffect hook of your useHover hook, it needs to add/remove both event listeners at the same time, when the ref's current component mounts and unmounts. The key part is the hook needs to cache the current ref within the effect hook in order for the cleanup function to correctly function.
The second issue is you aren't removing the listener in the returned effect hook cleanup function.
The third issue is that EventTarget.addEventListener() returns undefined, which is a falsey value, thus your hook never calls modalOpen or modalClose
The last issue is with the modal open/close state/callbacks being coupled to the useHover hook's implementation. (this is fine, but with this level of coupling you may as well just put the hook logic directly in the parent component, completely defeating the point of factoring it out into a reusable hook!)
Solution
Here's what I was able to get working:
const useHover = () => {
const ref = useRef();
const _ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current) {
_ref.current = ref.current; // cache external ref value for cleanup use
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
}
return () => {
if (_ref.current) {
_ref.current.removeEventLisener("mouseenter", enter);
_ref.current.removeEventLisener("mouseleave", leave);
}
};
}, []);
return [ref, hovered];
};
Note: using this with a modal appears to have interaction issues as I suspected, but perhaps your modal works better.