UseEffect won't use updated state - javascript

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).

Related

Make useEffect only run when component is in view (i.e. scrolled down to that component)

i have this code that updates a number from 2019 to 2022 but i want it to only run when the component is in view, meaning if they haven't scrolled to that part of the page the effect shouldn't run
useEffect(() => {
setTimeout(() => {
setTitle(2020)
}, 2000)
setTimeout(() => {
setTitle(2021)
}, 4000)
setTimeout(() => {
setTitle(2022)
}, 6000)
}, []);
at the moment it updates the value even when you haven't scrolled down to that part of the page, is there a way to make useEffect only activate when scrolled to that section of the page or do you need to do it completely differently?
window.addEventListener('scroll', function() {
var element = document.querySelector('#main-container');
var position = element.getBoundingClientRect();
// checking whether fully visible
if(position.top >= 0 && position.bottom <= window.innerHeight) {
console.log('Element is fully visible in screen');
}
// checking for partial visibility
if(position.top < window.innerHeight && position.bottom >= 0) {
console.log('Element is partially visible in screen');
}
});
Snippet from https://usefulangle.com/post/113/javascript-detecting-element-visible-during-scroll. Paste your code to the part where an element is fully in view. Remember to query select the element you want to have in view first.

Window.scroll only working once (React.js)

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);
};

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)

How to add a window event listener (within a functional component) that calls back function received as props

I'm working on my first React web app and got stuck creating a Knob/dial component that will be re-used hundreds of times throughout the app.
The user interaction will be handled within 3 elements, the Knob, the Knob's Parent, and the window object.
The Knob will receive a value as props, and represent this value visually, and it will also allow user interaction, changing the value dynamically with click and drag mouse movements.
The user interaction expected is:
< Knob onMouseDown > -> will set the Knob state "isChanging" to true, store mouse coordinates in the Parent state, and add 2 event listeners in the window object:
window.AddEventListener('onmousemove' -> if Knob state "isChanging" is true, calculate the new value (calling back a function passed as props to the Knob), and store it in the Parent state)
window.addEventListener('onmouseup' -> set Knob state "isChanging" to false)
The Knob will receive the value property dynamically (as part of the Parent state). I also wanna pass, as props to the Knob, the callback functions that will handle the mouse movements.
The thing is that I'm having trouble getting the window events to work..
Although I find out that the onMouseDown callback is working fine (altering the Knob state, and the mouseAt state value in the Parent), nothing that is set in the window event listeners is being executed when I move the mouse after clicking down in the Knob.
atm the code is as follows
class Parent extends Component {
constructor(props) {
super(props);
this.state={
value: 10,
min: 0,
max: 100,
mouseAt: null;
}
}
calcValue = (operation) => {
if(operation === 'increase') {
// Increase logic here
// Will sure have some call of this.setState({value: x})
}
if( operation === 'decrease') {
// Decrease logic here
// Will sure have some call of this.setState({value: x})
}
}
onMouseMove = (e) => {
if (e.clientY < this.state.mouseAt) {
this.calcValue('increase');
}
if (e.clientY > this.state.mouseAt) {
this.calcValue('decrease');
}
this.setState({mouseAt: e.clientY});
};
startMove = (e) => {
this.setState({mouseAt: e.clientY});
}
render() {
return (
<div>
<Knob windowMouseMove={this.onMouseMove} startMove={this.startMove} value={this.state.value) min={this.state.min} max={this.state.max} radius={15}></Knob>
</div>
)
}
}
and then in the Knob component:
function describeArc(x, y, radius, startAngle, endAngle){
// return svg path code
}
const Knob = (props) => {
const [isChanging, setChangeState] = useState(false);
useEffect(() => {
if (isChanging) {
window.addEventListener('onmousemove', props.windowMouseMove);
window.addEventListener('onmouseup', stopMoving);
};
return () => {
if (!isChanging) {
window.removeEventListener('mousemove', props.windowMouseMove);
window.removeEventListener('mouseup', stopMoving);
}
}
}, [isChanging])
const startMoving = (e) => {
setChangeState(true);
props.startMove(e);
}
const stopMoving = () => {
setChangeState(false);
}
return (
<svg onMouseDown={startMoving}>
<path id="arc1" fill="none" stroke="446688" strokeWidth="5"
d={describeArc(27.5, 27.5, props.radius, -160, 160)}/>
<path id="arc2" fill="none" stroke="#4169' strokeWidth="5"
d={decribeArc(27.5, 27.5, props.radius, radProps(props.value, props.min, props.max), 160)}/>
</svg>
)
}
I also have tried adding the window event listeners directly in the onMouseDown callback, but without success.
Tracking the state between callback calls, I found that the last callback being executed is the one in the body of useEffect, with "isChanging" set to true, even though no window event listener is being executed.
Any help on how to pull this off is appreciated.
Thanks.
[SOLVED] - The solution was simple, instead of window.addEventListener, I used window.onmousemove = props.windowMouseMove, and window.onmouseup = stopMoving, and before unmounting window.onmousemove = null and window.onmouseup = null.

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]);

Categories

Resources