Window.scroll only working once (React.js) - javascript

I'm making a simple React component to remember where you are on the page and place you there when returning.
Here's the code:
function ScrollCache() {
window.scroll(0, parseInt(localStorage['scroll']));
document.addEventListener('scroll', function (e) {
if (window.scrollY != 0) {
localStorage['scroll'] = window.scrollY.toString();
}
})
return (<></>)
}
Basically, it caches the last known scroll position, and uses window.scroll(x, y) to scroll to that position. I have verified that localStorage is working as intended with a console.log immediately before the window.scroll. I've also just tried a static 100 for the y coordinate. No matter what, it only scrolls once at reload and then never again when I'm navigating around.
I'm using React Router to go between web pages. Any help is appreciated

You don't need to add the scroll event listener every time you want to cache the scroll.
Instead, try this:
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
window.addEventListener("scroll", handleScroll, {
passive: true
});
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [scrollPosition]);
useEffect(() => {
localStorage['scroll'] = scrollPosition.toString();
}, [scrollPosition);
const handleScroll = () => {
const position = window.pageYOffset;
setScrollPosition(position);
};

Related

Adding an event listener with useEffect hook for scrolling

I am using the useEffect hook below to sense the page scroll but it is not logging "yes" to the console. What am I missing? My page is scrolling more than 50px so it should print out "yes".
useEffect(() => {
const scrollFun = window.addEventListener("scroll", () => {
if(window.pageYOffset%50 === 0) {
console.log("yes");
}
});
return () => {
window.removeEventListener("scroll", scrollFun);
};
}, []);
It's still a bit unclear if you want just a single "page has scrolled 50px" or if you are looking for continual monitoring of scrolling every 50px so I'll try to answer both and you can choose which ever is closer.
Scenario 1 - Scroll first 50px and log
This one is trivial. I suggest using a React ref to "capture" having scrolled the page by at least 50px, resetting when scrolled back.
const scrolled50Ref = useRef();
useEffect(() => {
const scrollFun = () => {
if (window.pageYOffset > 50) {
if (!scrolled50Ref.current) {
scrolled50Ref.current = true;
console.log('scrolled 50 px');
}
} else {
scrolled50Ref.current = false;
}
}
window.addEventListener("scroll", scrollFun);
return () => {
window.removeEventListener("scroll", scrollFun);
};
}, []);
Scenario 2 - Log every 50px of scrolling
This one isn't as trivial and likely what you are seeing. The issue here is that many browsers use accelerated scrolling, so you won't hit every pageYOffset value. In other words, after scrolling a certain speed/velocity you are likely not to hit an pageYOffset evenly divisible by 50.
The "fix" here is to increase the "window" of divisible by 50.
useEffect(() => {
const scrollFun = () => {
if (window.pageYOffset % 50 >= 45) {
console.log("scrolled 50-ish pixels");
}
}
window.addEventListener("scroll", scrollFun);
return () => {
window.removeEventListener("scroll", scrollFun);
};
}, []);
Demo
Change your if condition with this one:
if (window.pageYOffset > 0)

UseEffect won't use updated state

I have a react function which is supposed to be my header. This header shall change its background color after reaching a button on scroll.
To do so I use a scroll event listener and track its position in relation to the button. This works fine for setTransparent(false), but not for setTransparent(true): Logging transparent inside of the listener returns true even after setting it to false inside of the first if-statement.
How so? What's the best practice here?
const [transparent, setTransparent] = useState(true);
useEffect(() => {
const button = document.querySelector(".hero-button");
window.addEventListener("scroll", () => {
const {bottom} = button.getBoundingClientRect();
if (transparent && bottom <= 0) {
setTransparent(false);
} else if (!transparent && bottom > 0) {
setTransparent(true);
}
});
}, [])
Setting the dependency to transparent will make it work, but this will add even listener every time it updates.
Your transparent variable in the effect callback only references the value on the initial render, which is always true. You can fix it by re-adding the scroll listener whenever transparent changes, and return a cleanup function that removes the prior handler:
useEffect(() => {
const button = document.querySelector(".hero-button");
const scrollHandler = () => {
const { bottom } = button.getBoundingClientRect();
if (transparent && bottom <= 0) {
setTransparent(false);
} else if (!transparent && bottom > 0) {
setTransparent(true);
}
};
window.addEventListener("scroll", scrollHandler);
// DON'T FORGET THE NEXT LINE
return () => window.removeEventListener("scroll", scrollHandler);
}, [transparent]);
Another option would be to use a ref instead of useState for transparent (or, in addition to useState if transparent changes needs to result in re-rendering).

Removing eventHandler in function called by addEventListener

I added an infinite scrolling feature to my page. It attaches an event listener in the componentDidMount lifecycle hook and I want to remove it within the action called by the event listener when there is no "nextlink anymore". I set a console.log() message which works fine, but I am uncertain why the window.removeEventListener() function does not work. Any help would be appreciated.
Piece of code responsible for adding/removing the eventListener.
componentDidMount() {
this._isMounted = true;
this.props.onFetchTeams();
this.scrollListener = window.addEventListener("scroll", e => {
this.handleScroll(e);
});
}
handleScroll = () => {
const hasMoreLink = this.props.teams["#odata.nextLink"];
if (hasMoreLink == "") {
console.log("remove event handler");
window.removeEventListener("scroll", this.handleScroll);
}
// If there is at least a team and is currently not loading, proceed to load more.
if (this.state.loadingMore === false && this.props.teams["value"]) {
// get the last teamcard in the page
const lastTeam = document.querySelector(
".team-card-wrapper:last-of-type"
);
// get the height of the current team, and get the height of the current position on screen.
const lastTeamOffset = lastTeam.offsetTop + lastTeam.clientHeight;
const pageOffset = window.pageYOffset + window.innerHeight;
// the range that teams will load earlier than the bottom of the page.
const bottomOffset = 30;
if (pageOffset > lastTeamOffset - bottomOffset) {
this.setState({ loadingMore: true });
this.props.onFetchMoreTeams(hasMoreLink);
}
}
};
removeListener needs the same reference for function that it used while addListener. Change the code to addEventListener like
this.scrollListener = window.addEventListener("scroll", this.handleScroll);
This is because the function that is given to addEventListener and the one given to the removeEventListener should be exactly the same, but in your case, you are creating a new arrow function for the addEventListener. so I think you should try something like this:
this.scrollListener = window.addEventListener("scroll", this.handleScroll)
...
handleScroll = (e) => {
...
if(noMoreScroll) window.removeEventListener("scroll", this.handleScroll)
...
}
I hope this helps you :)
Consider revising the way your add the scroll event handler, by passing the handleScroll function directly:
componentDidMount() {
this._isMounted = true;
this.props.onFetchTeams();
/*
With the this.handleScroll bound to this class instance, we can now pass the method
directly to addEventListener as shown
*/
this.scrollListener = window.addEventListener("scroll", this.handleScroll);
}
handleScroll = () => {
const hasMoreLink = this.props.teams["#odata.nextLink"];
if (hasMoreLink == "") {
console.log("remove event handler");
/* This will now work as expected */
window.removeEventListener("scroll", this.handleScroll);
}
/* Rest of your code remains unchanged .. */
}

How to remove scroll event listener?

I am trying to remove scroll event listener when I scroll to some element. What I am trying to do is call a click event when some elements are in a viewport. The problem is that the click event keeps calling all the time or after first call not at all. (Sorry - difficult to explain) and I would like to remove the scroll event to stop calling the click function.
My code:
window.addEventListener('scroll', () => {
window.onscroll = slideMenu;
// offsetTop - the distance of the current element relative to the top;
if (window.scrollY > elementTarget.offsetTop) {
const scrolledPx = (window.scrollY - elementTarget.offsetTop);
// going forward one step
if (scrolledPx < viewportHeight) {
// console.log('section one');
const link = document.getElementById('2');
if (link.stopclik === undefined) {
link.click();
link.stopclik = true;
}
}
// SECOND STEP
if (viewportHeight < scrolledPx && (viewportHeight * 2) > scrolledPx) {
console.log('section two');
// Initial state
let scrollPos = 0;
window.addEventListener('scroll', () => {
if ((document.body.getBoundingClientRect()).top > scrollPos) { // UP
const link1 = document.getElementById('1');
link1.stopclik = undefined;
if (link1.stopclik === undefined) {
link1.click();
link1.stopclik = true;
}
} else {
console.log('down');
}
// saves the new position for iteration.
scrollPos = (document.body.getBoundingClientRect()).top;
});
}
if ((viewportHeight * 2) < scrolledPx && (viewportHeight * 3) > scrolledPx) {
console.log('section three');
}
const moveInPercent = scrolledPx / base;
const move = -1 * (moveInPercent);
innerWrapper.style.transform = `translate(${move}%)`;
}
});
You can only remove event listeners on external functions. You cannot remove event listeners on anonymous functions, like you have used.
Replace this code
window.addEventListener('scroll', () => { ... };
and do this instead
window.addEventListener('scroll', someFunction);
Then move your function logic into the function
function someFunction() {
// add logic here
}
You can then remove the click listener when some condition is met i.e. when the element is in the viewport
window.removeEventListener('scroll', someFunction);
Instead of listening to scroll event you should try using Intersection Observer (IO) Listening to scroll event and calculating the position of elements on each scroll can be bad for performance. With IO you can use a callback function whenever two elements on the page are intersecting with each other or intersecting with the viewport.
To use IO you first have to specify the options for IO. Since you want to check if your element is in view, leave the root element out.
let options = {
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
Then you specify which elements you want to watch:
let target = slideMenu; //document.querySelector('#oneElement') or document.querySelectorAll('.multiple-elements')
observer.observe(target); // if you have multiple elements, loop through them to add observer
Lastly you have to define what should happen once the element is in the viewport:
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
});
};
You can also unobserve an element if you don't need the observer anymore.
Check this polyfill from w3c to support older browsers.
Here is my scenario/code, call removeEventListener as return() in the useEffect hook.
const detectPageScroll = () => {
if (window.pageYOffset > YOFFSET && showDrawer) {
// do something
}
};
React.useEffect(() => {
if (showDrawer) {
window.addEventListener("scroll", detectPageScroll);
}
return () => {
window.removeEventListener("scroll", detectPageScroll);
};
}, [showDrawer]);

In Vue app, detecting that a key press or mouse click happens

In my Vue app, I have a place where I'm setting a timer. I'd like to interrupt the timer if the user presses any key or clicks anywhere in the browser window.
I do not want to stop whatever they clicked from happening. I just want to get a callback whenever it does.
I could just put a function call in every handler, but that is both tedious, sloppy, and not very maintainable.
This is what I'm trying to achieve:
let timerId;
const startTheTimer = () => {
timerId = setTimeout(() => {
somestuff();
timerId = undefined;
}, 10000);
}
// Called whenever any mouse click or keyboard event happens
const userDidSomething = () => {
if (timerId) {
clearTimeout(timerId);
timerId = undefined;
}
}
So, my question is, how do I get the function userDidSomething() to be called?
Thanks.
Your question doesn't seem to have much to do with Vue. You'd just need to attach an event listener to document. For example
document.addEventListener('click', userDidSomething)
document.addEventListener('keydown', userDidSomething)
Note: Any event that is caught and has stopPropagation() called will not reach the document.
If your timer is set within a component, you should probably do all this in your component's mounted lifecycle hook.
It would also be a good idea to clear the timeouts and remove the listeners in the beforeDestroy hook.
For example
export default {
data () {
return {
timerId: null
}
},
methods: {
startTimer () {
this.timerId = setTimeout(...)
},
clearTimer () {
clearTimeout(this.timerId)
}
},
mounted () {
this.startTimer() // assuming you want to start the timer on mount
document.addEventListener('click', this.clearTimer)
document.addEventListener('keydown', this.clearTimer)
},
beforeDestroy () {
this.clearTimer()
document.removeEventListener('click', this.clearTimer)
document.removeEventListener('keydown', this.clearTimer)
}
}

Categories

Resources