I'm working on adding a class to the DOM when the scroll position reaches a certain number. If the number is 150 or above, a class gets added. If the number is less than 150, the class gets removed. Currently, when I scroll down, all is well and the class gets added. However, when you scroll back up, if the number reaches a certain point, the scrollTop number bounces from one number to another repetitively. This is my code:
The Functionality
const useVisibilityHook = threshold => {
const [visible, setVisible] = useState(false);
useEffect(() => {
const getPos = () => {
const currentPos = window.pageYOffset;
console.log(currentPos, threshold);
setVisible(currentPos > threshold);
}
window.addEventListener('scroll', getPos);
return () => {
window.removeEventListener('scroll', getPos);
};
}, []);
return visible;
};
const Banner = () => {
const isVisible = useVisibilityHook(150);
return (
<div className={`violator ${isVisible ? 'hide' : ''}`}>
What to update
className={`violator ${!visible ? 'hide' : ''}`}
I'm not sure the best way to describe what I'm seeing, but basically. The currentPos variable will flip between a number like 124 and 292 respectively. The numbers certainly change as you slowly scroll the page back up, but you get the idea. This causes the class to toggle, which is no good.
This is what I see over and over again
You are adding and removing scroll event listener on each render (whenever you set the state). You should lock the dependencies of the useEffect block, so it will only be called once.
I've create a custom hook useVisibilityHook that should do what I assume you want (I've used 300 as threshold to make the change point clearer):
const { useState, useEffect } = React;
const useVisibilityHook = threshold => {
const [visible, setVisible] = useState(false);
useEffect(() => {
const getPos = () => {
const currentPos = window.pageYOffset;
setVisible(currentPos > threshold);
}
window.addEventListener('scroll', getPos);
return () => {
window.removeEventListener('scroll', getPos);
};
getPos();
}, []);
return visible;
};
const App = () => {
const isVisible = useVisibilityHook(300);
return (
<div className={`app ${isVisible ? 'show' : ''}`} />
);
};
ReactDOM.render(
<App />,
root
);
.app {
height: 5000px;
background: blue;
visibility: hidden;
}
.show {
visibility: visible;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Related
I want to change the source of image onscroll in reactjs. Like if scrollY is greater than 100 change the image source and if it is greater than 200 change it another source.
i tried to do it but could not. any ideas?
import React, { useEffect, useState, useRef } from 'react';
import './Video.css';
import { useInView } from 'react-intersection-observer';
function Video() {
const videoSrc1 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/6341303c29c5340961dc9ae6_Mco-1-transcode.mp4";
const videoSrc2 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-transcode.mp4";
const videoSrc3 = "https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-transcode.mp4";
const img1 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg';
const img2 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-poster-00001.jpg';
const img3 = 'https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg';
const [scrollPosition, setScrollPosition] = useState(0);
const handleScroll = () => {
const position = window.pageYOffset;
setScrollPosition(position);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
{
if (scrollPosition>=316){
// this.src={videoSrc2}
}
}
console.log("position;", scrollPosition)
return (
<div className='container'>
<video loop autoPlay muted className='video'>
<source src={videoSrc1} type="video/webm" />
</video>
</div>
)
}
export default Video
You can do a combination of Vanilla JS methods and React methods to achieve this.
Your best best bet is to use the useEffect hook and add an event listener to the Window DOM object based on where on the page the scroll is position.
First you need a function that executes every time the DOM re-renders (scrolling does this)
start by using the useEffect hook
useEffect(() => {}, [])
Next you want a function that executes specifically when you scroll the page
you can add an event handler to the window DOM element
window.addEventListener('scroll',() => {})
Then you want to track the where you are on the page (how far up or how far down)
You can use the window's scrollTop property to return how far up or down you are on the page relative to the top of the page
document.documentElement.scrollTop
Now comes the logic part, you said you want to change the image's src based on how far up or down you've scrolled on the page
This is where, useState, boolean flags and the ternary operator come into play
You can write a useState hook to store the Y position of the scroll, and the useEffect and scroll event listener will keep updating it to the current position
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
finally nest the hook function into the window 'scroll' function and nest that in the useEffect hook
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
useEffect(() => {
window.addEventListener('scroll',() => {
getScrollPositon(document.documentElement.scrollTop);
})
}, [])
AND finally write the logic in your .jsx code to say 'when we are x number of pixel below the top of the screen...change the image source'
const App = () => {
return (
<div className='app'>
<img src={scrollPosition < 1000 ? 'http://imagelinkA.com' : 'http://imagelinkB.com'}>
</div>
);
}
Now you put it all together...
// App.js/jsx
import { useState, useEffect } from 'react';
const App = () => {
// initial scroll positon on page load
const [scrollPosition, getScrollPositon] = useState(document.documentElement.scrollTop)
// hook and event handlers to keep track of and update scroll
useEffect(() => {
window.addEventListener('scroll',() => {
getScrollPositon(document.documentElement.scrollTop);
})
}, [])
// your .jsx code with appropriate boolean flags and use of the ternary operator
return (
<div className='app'>
<img src={scrollPosition < 1000 ? 'http://imagelinkA.com' : 'http://imagelinkB.com'}>
</div>
);
}
Hope I was able to help!
Setting the scroll position will trigger needless rerenders, instead you only want to trigger a rerender when the data source will change.
To select the proper data source, putting the list of data sources in a list is a good way to do this. Then you can properly determine the index of data source to show with something like this:
// Y_OFFSET_DIFFERENCE is the value that determines when the next image should be shown.
const index =
Math.floor(position / Y_OFFSET_DIFFERENCE) % dataSources.length;
You can see how this is properly calculated:
If position = 0 and Y_OFFSET_DIFFERENCE = 100 then 0/100 = 0 and 0 % 2 is 0. 0 is the index of the first element of your list.
If position = 100 and Y_OFFSET_DIFFERENCE = 100 then 100/100 = 1 and 1 % 2 is 1. 1 is the index of the second element in your list.
If position = 150 and Y_OFFSET_DIFFERENCE = 100 then 150/100 = 1.5 and Math.floor(1.5) = 1 and 1 % 2 is 1. 1 is the index of the second element in your list.
If position = 200 and Y_OFFSET_DIFFERENCE = 100 then 200/100 = 2 and 2 % 2 is 0. 0 is the index of the first element in your list.
And it'll continue like this forever.
Here is the full code.
import { useState, useEffect } from "react";
const dataSources = [
"https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63455a67996ba248148c4e31_add-options%20(3)-poster-00001.jpg",
"https://global-uploads.webflow.com/62efc7cb58ad153bfb146988/63413ff244f1dc616b7148a0_Mco-poster-00001.jpg"
];
const DEFAULT_DATA_SOURCE = dataSources[0];
const Y_OFFSET_DIFFERENCE = 100;
export default function App() {
const [dataSource, setDataSource] = useState(DEFAULT_DATA_SOURCE);
useEffect(() => {
const handleScroll = () => {
const position = window.pageYOffset;
const index =
Math.floor(position / Y_OFFSET_DIFFERENCE) % dataSources.length;
const selectedSource = dataSources[index];
if (selectedSource === dataSource) {
return;
}
setDataSource(selectedSource);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [dataSource]);
return (
<div
style={{ height: 2000, backgroundImage: "linear-gradient(blue, green)" }}
>
<div
style={{
position: "sticky",
top: 10,
left: 10,
display: "flex",
justifyContent: "center",
flexDirection: "column",
alignItems: "center"
}}
>
<p style={{ color: "white", textAlign: "center" }}>{dataSource}</p>
<img
src={dataSource}
alt="currently selected source"
width={100}
height={100}
/>
</div>
</div>
);
}
codesandbox demo
I've seen tutorials that demonstrates how to do this but all that I've seen uses "window" or examples where the element comes in view from the bottom of a page. I have a modal with a fixed height that scrolls:
const ItemMarkup = forwardRef(({user}, ref) => <li ref={ref}>...</li>)
// This opens up in a modal: https://headlessui.dev/react/dialog
<div className="content">
{/** Tailwind's fixed height 'h-96' */}
<ul ref={scrollRef} className="h-96 overflow-scroll">
{users.map((user, index) =>
<ItemMarkup
{/** I'm looking for the last item/element to come in this view (ul) */}
ref={users.length === index + 1 ? ref : null}
key={user.id}
user={user}
/>
)}
</ul>
</div>
I've looked at this demo but again, it's for the entire page/window. Then I've tried using this function:
// element would be the ref.current
// target would be scrollRef
// But this always return false
function isInViewport(element, target) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= target.innerHeight &&
rect.right <= target.innerWidth
);
}
Here's how I tried the attempt:
const ref = useRef()
const scrollRef = useRef()
// From the demo link above
const onScreen = useOnScreen(ref)
// No idea if I need this
const [scrolling, setScrolling] = useState(false)
useEffect(() => {
const onScroll = e => {
setScrolling(true)
console.log(isInViewport(ref.current, scrollRef))
};
scrollRef?.current.addEventListener("scroll", onScroll);
//console.log(ref, onScreen)
return () => scrollRef?.current.removeEventListener("scroll", onScroll);
}, [ref, onScreen, scrolling])
This seems all wrong. Is there a react hook I could to determine when a specific element comes in a modal view or any scrollable view that I specify and not the document view? I just cannot find any documentation/tutorials on this.
I'm creating an infinite scroll. When the last items comes in view, of the modal, I then trigger a function to fetch more data.
I guess I have over complicated things:
useEffect(() => {
const onScroll = e => {
console.log(isInViewport(e.target))
};
scrollRef?.current.addEventListener("scroll", onScroll);
return () => scrollRef?.current.removeEventListener("scroll", onScroll);
}, [])
function isInViewport(element) {
return element.scrollHeight - element.scrollTop === element.clientHeight
}
Now when I scroll to the bottom, I see a console log of true.
I'm working on a chat app and am using the scroller from bottom to top to load older messages.
When a new message arrives I want to check first if the user is at the bottom of the div, and only then use a scrollToBottom function.
How can I get the current height/position of the user?
https://www.npmjs.com/package/react-infinite-scroller
Thank you,
Omri
Unfortunately it's been a few days without reply. This is my workaround:
I created a boolean called isBottom, and attached onScroll={handleScroll} function to my messages div.
const [isBottom, setIsBottom] = useState(true);
const scrollToBottom = (behavior) => {
messagesEndRef.current.scrollIntoView();
};
const handleScroll = (e) => {
const bottom =
e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
if (bottom) {
setIsBottom(true);
} else {
setIsBottom(false);
}
};
The messages div:
<div className="msg_list" onScroll={handleScroll}>
<InfiniteScroll
loadMore={loadMore}
initialLoad={true}
hasMore={hasMoreItems}
loader={<LoadingAnimation key={0} />}
useWindow={false}
isReverse={true}
>
{messages}
<div ref={messagesEndRef} />
</InfiniteScroll>
</div>
And then I added a useEffect to handle changes from my messages array (arriving from props)
useEffect(() => {
if (isBottom) {
scrollToBottom();
} else {
setUnreadMessages((unreadMessages) => unreadMessages + 1);
}
}, [messageList]);
* BTW you also need to wire the scrollTobottom function to your send message box, since if you are the one who sent the message it should scrollToBottom anyway
I'm having a problem with inline style changes that are reset when my dispatch is finished, because the state is being re-rendered, despite the other functionality of my component is working (you can still see that the counter is not stopping).
Here's a demonstration of what I mean.
You can see that the orange bar of the left box vanishes when the orange bar of the right bar finishes (the animation ends). Essentially what I'm doing here is changing the width property in inline styles.
import React, { useEffect, useRef } from "react";
import { useDispatch, connect } from "react-redux";
import { addProfessionExperience } from "../../actions/index";
import "./Professions.sass";
const timers = [];
const progressWidths = [];
const mapStateToProps = (state, ownProps) => {
const filterID = +ownProps.match.params.filter;
const { professions, professionExperience } = state;
return {
professions: professions.find(item => item.id === filterID),
professionExperience: professionExperience
};
};
const produceResource = (dispatch, profession, sub, subRef) => {
if(timers[sub.id]) return;
/*
* Begin the progress bar animation/width-change.
*/
Object.assign(subRef.current[sub.id].style, {
width: "100%",
transitionDuration: `${sub.duration}s`
});
/*
* Updates the progress text with the remaining time left until done.
*/
let timeLeft = sub.duration;
const timeLeftCountdown = _ => {
timeLeft--;
timeLeft > 0 ? setTimeout(timeLeftCountdown, 1000) : timeLeft = sub.duration;
subRef.current[sub.id].parentElement.setAttribute("data-duration", timeLeft + "s");
}
setTimeout(timeLeftCountdown, 1000);
/*
* Dispatch the added experience from profession ID and sub-profession level.
* We do not allow duplicate timers, only one can be run at a time.
*/
const timer = setTimeout(() => {
Object.assign(subRef.current[sub.id].style, {
width: "0%",
transitionDuration: "0.2s"
});
dispatch(addProfessionExperience({ id: profession.id, level: sub.level }));
delete timers[sub.id];
}, sub.duration * 1000);
timers[sub.id] = timer;
};
const isSubUnlocked = (professionMaxExperience, subLevel, professionExperience) => {
if(professionExperience <= 0 && subLevel > 1) return false;
return professionExperience >= getExperienceThreshold(professionMaxExperience, subLevel);
};
const getExperienceThreshold = (professionMaxExperience, subLevel) => (((subLevel - 1) * 1) * (professionMaxExperience / 10) * subLevel);
const ConnectedList = ({ professions, professionExperience }) => {
const currentExperience = professionExperience.find(item => item.profession === professions.id);
const subRef = useRef([]);
const dispatch = useDispatch();
useEffect(() => {
subRef.current = subRef.current.slice(0, professions.subProfessions.length);
}, [professions.subProfessions]);
return (
<div>
<div className="list">
<ul>
{professions.subProfessions.map(el => {
const unlocked = isSubUnlocked(
professions.maxExperience,
el.level,
(currentExperience ? currentExperience.amount : 0)
);
const remainingExperience = getExperienceThreshold(professions.maxExperience, el.level) - (currentExperience ? currentExperience.amount : 0);
return (
<li
key={Math.random()}
style={{ "opacity": unlocked ? "1" : "0.5" }}
>
<div className="sprite">
<img alt="" src={`/images/professions/${el.image}.png`} />
</div>
<div className="caption">{el.name}</div>
<div
className="progress-bar"
data-duration={unlocked ? `${el.duration}s` : `${remainingExperience} XP to Unlock`}
data-identifier={el.id}
>
<span ref={r => subRef.current[el.id] = r} ></span>
</div>
<div className="footer">
<button
className="btn"
onClick={() => unlocked ? produceResource(dispatch, professions, el, subRef) : false}
>
{unlocked ?
`Click` :
<i className="fa fa-lock"></i>
}
</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
};
const List = connect(mapStateToProps)(ConnectedList);
export default List;
How can I make it so the orange bars persist on their own and not disappears when another one finishes?
One problem is that you're using Math.random() to generate your keys. Keys are what the virtual DOM uses to determine whether an element is the "same" as the one on a previous render. By using a random key, you're telling the virtual DOM that you want to spit out a brand new DOM element instead of reusing the prior one, which means the new one won't retain any of the side effects you placed on the original element. Read up on React's reconciliation for more info on this.
Try to use keys that logically represent the thing you're rendering. In the case of your code, el.id looks like it may be a unique identifier for the subprofession you're rendering. Use that for the key instead of Math.random().
Additionally, refs are going to make reasoning about your code really difficult. Rather than using refs to manipulate your DOM, use state manipulation and prop passing, and let React re-render your elements with the new attributes.
I want the div element to get the class of "showtext" when you scroll 100 pixels or less above the element. When you're 100 pixels or more above it, it has the class of "hidden".
I am trying to use a ref to access the div element, and use a method called showText to check and see when we scroll to 100 pixels or less above that div element, i'm using scrollTop for this.
Then i use componentDidMount to add a window event listener of scroll, and call my showText method.
I am new to this, so I am sure there is mistakes here and probably bad code. But any help is appreciated!
import React, {Component} from 'react';
class SlideIn extends Component{
state={
showTexts: false,
}
showText=()=>{
const node= this.showTextRef;
if(node.scollTop<=100)
this.setState({
showTexts: true
})
}
componentDidMount(){
window.addEventListener('scroll', this.showText() )
}
render(){
const intro= document.querySelector('.intro')
return(
<div classname={this.state.showTexts ? 'showText' : 'hidden'} ref={node =>this.showTextRef = node}>
{window.addEventListener('scroll', this.showText)}
<h1>You did it!</h1>
</div>
)
}
}
export default SlideIn
I have tried using this.showText in my window scroll event, and as you see above this.showText(), neither have worked. I tried to use the current property on my div ref in my showText method, and it threw a error saying the scrollTop could not define the property of null.
Again I am new to this and have never added a window event listener this way, nor have I ever used scrollTop.
Thanks for any help!
When you attach an event listener you have to pass a function as a parameter. You are calling the function directly when you add the event listener.
In essence, you need to change:
componentDidMount(){
window.addEventListener('scroll', this.showText() )
}
to:
componentDidMount(){
window.addEventListener('scroll', this.showText)
}
In your scroll listener you should check the scroll position of the window(which is the element where you are performing the scroll):
showText = () => {
if (window.scrollY <= 100) {
this.setState({
showTexts: true
});
}
}
Also, you are attaching the event listener in the render method. The render method should only contain logic to render the elements.
Pass function as parameter like
window.addEventListener('scroll', this.showText)
and remove it from return.
Then you just need to do only this in function
if(window.scrollY<=100)
this.setState({
showTexts: true
})
use your div position here
You need to use getBoundingCLientRect() to get scroll position.
window.addEventListener("scroll", this.showText); you need to pass this.showText instead of calling it.
classname has speeling mistake.
showText = () => {
const node = this.showTextRef;
const {
y = 0
} = (node && node.getBoundingClientRect()) || {};
this.setState({
showTexts: y <= 100
});
};
componentDidMount() {
window.addEventListener("scroll", this.showText);
}
render() {
const intro = document.querySelector(".intro");
return (
<div
className={this.state.showTexts ? "showText" : "hidden"}
ref={node => (this.showTextRef = node)}
>
<h1>You did it!</h1>
</div>
);
}
condesandbox of working example: https://codesandbox.io/s/intelligent-shannon-1p6sp
I've put together a working sample for you to reference, here's the link: https://codesandbox.io/embed/summer-forest-cksfh
There are few things to point out here in your code:
componentDidMount(){
window.addEventListener('scroll', this.showText() )
}
Just like mgracia has mentioned, using this.showText() means you're directly calling the function. The right way is just to use this.showText.
In showText function, the idea is you have to get how far user has scrolled from the top position of document. As it was called using:
const top = window.pageYOffset || document.documentElement.scrollTop;
now it's safe to check for your logic and set state according to the value you want, here I have put it like this:
this.setState({
showTexts: top <= 100
})
In your componentDidMount, you have to call showText once to trigger the first time page loading, otherwise when you reload the page it won't trigger the function.
Hope this help
Full code:
class SlideIn extends Component {
state = {
showTexts: false,
}
showText = () => {
// get how many px we've scrolled
const top = window.pageYOffset || document.documentElement.scrollTop;
this.setState({
showTexts: top <= 100
})
}
componentDidMount() {
window.addEventListener('scroll', this.showText)
this.showText();
}
render() {
return (
<div className={`box ${this.state.showTexts ? 'visible' : 'hidden'}`}
ref={node => this.showTextRef = node}>
<h1>You did it!</h1>
</div>
)
}
}
.App {
font-family: sans-serif;
text-align: center;
height: 2500px;
}
.box {
width: 100px;
height: 100px;
background: blue;
position: fixed;
left: 10px;
top: 10px;
z-index: 10;
}
.visible {
display: block;
}
.hidden {
display: none;
}