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
Related
Simple React component.
SVG element with onClick handler.
For some reason useLayoutEffect "see" DOM changes BEFORE it applied to real DOM.
How to make it "see" actual DOM?
Here is what happens:
1 onClick handler trigger useState function and mutate color variable.
2 color variable change trigger useLayoutEffect and useEffect
3 useLayoutEffect holds script execution for 3 seconds
3.1 For some reason console.log show new state of HTML element. Thats strange.
4 changed DOM come to place, i.e element rendered with new color in style object.
My question:
Why useLayoutEffect show new element style BEFORE it actually changed?
Here is React comnponent source:
const sleep = (duration: number) => {
let start = new Date().getTime();
let end = start;
while (end < start + duration) {
end = new Date().getTime();
}
};
const Logo = () => {
const [color, setColor] = useState<string>("red");
const logoRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
console.log(`useLayoutEffect BEFORE sleep(3sec) getElementById color=${document?.getElementById("logowrapper")?.style.fill}`);
console.log(`useLayoutEffect BEFORE sleep(3sec) logoRef color=${logoRef.current?.style.fill}`);
sleep(3000);
console.log(`useLayoutEffect AFTER sleep(3sec) getElementById color=${document?.getElementById("logowrapper")?.style.fill}`);
console.log(`useLayoutEffect AFTER sleep(3sec) logoRef color=${logoRef.current?.style.fill}`);
}, [color]);
useEffect(() => {
console.log(`useEffect color=${document?.getElementById("logowrapper")?.style.fill}`);
}, [color]);
const changeColor = () => {
setColor((color): string => (color === "red" ? "green" : "red"));
};
return (
<div
onClick={() => changeColor()}
className="App-logo"
style={{ fill: color }}
id="logowrapper"
ref={logoRef}
>
<LogoSvg />
</div>
);
// return <img src={logo} className="App-logo" alt="logo" onClick={changeColor} style={{fill: 'red'}}/>
};
Real life example here: https://artemu78.github.io/dropdown/ (click on react logo in the center and it will change color with 3 seconds delay)
Repo here: https://github.com/artemu78/webdev_test_ya_ar
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 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>
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.