Next JS custom carousel and looping/showing/hiding through items - javascript

I have a custom build Next JS carousel that keeps an index of the slide we are on and renders an image or video to the screen. It seems that because it is autoplaying and each slide gets removed and readded every 6 or so seconds the page downloads constant copies of the same video or image when it comes around again. Is this not the correct way of doing this?
So the important part is here
{slides.map((item, key) => (
<>
{key == this.state.currentImageIndex && (
<div>
It will remove and readd content every slide. Does it waste resrouces constantly downloading them? Is there a better way?
import React from 'react'
//Importing data to use as the slides
import { slides } from './data/slides'
import VideoSlide from './VideoSlide'
import ImageSlide from './ImageSlide'
let slideTimer = null
class Carousel extends React.Component {
constructor(props) {
super(props)
this.state = {
currentImageIndex: 0, //Array index of slide from /data/slides
slideTime: 6, //Timing duration of a slide
}
this.setMyInterval = this.setMyInterval.bind(this)
}
//Runs when this componenet is included in the page
componentDidMount() {
this.setMyInterval()
}
changeSlide(current) {
/*Don't run if we are just clicking the same slide number*/
if (current != this.state.currentImageIndex) {
this.setState({ currentImageIndex: current }) //Change to selected slide1
clearTimeout(slideTimer) //Reset the timer so it's not half way through the countdown when we select another slide
this.setMyInterval() //Rebuild the slide countdown so we are starting at 0 and in sync with our CSS animation
}
}
setMyInterval() {
slideTimer = setInterval(() => {
const lastIndex = slides.length - 1 //Check when we need to loop
const { currentImageIndex } = this.state //Get current slide from state
const shouldResetIndex = currentImageIndex === lastIndex
const index = shouldResetIndex ? 0 : currentImageIndex + 1 //Reset to 0 if we are at the end
this.setState({
currentImageIndex: index, //Set new slide as number obtained from above
})
}, this.state.slideTime * 1000) //Repeat every x seconds
}
componentWillUnmount() {
clearInterval(slideTimer)
}
render() {
return (
<>
<div className="backgroundSlide">
{slides.map((item, key) => (
<>
{key == this.state.currentImageIndex && (
<div>
<VideoSlide url={slides[0].video} />
{slides[this.state.currentImageIndex].image && (
<ImageSlide
url={slides[this.state.currentImageIndex].image}
timer={this.state.slideTime}
/>
)}
</div>
)}
</>
))}
</div>
</>
)
}
}
export default Carousel

It's not clear from the question that what kind of Carousel logic you have. Usually, we render the whole carousel elements and then let the carousel do its job. As an example consider this CSS only carousel,
https://css-tricks.com/css-only-carousel/
If you use such an approach, you can have all of your slides rendered one time and don't have to worry about the removal/addition of any slides.

Related

How to change image src on scroll in reactjs

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

how to create a react slider component

I was trying to make a react slider component but for some reasons it doesn't change/update the image on click instead it always displays the same picture. What i'm trying to achieve is when I click the button it updates index and the display image changes according to index position. Even though the index is updating the image is not changing. My attempt at creating the slider component -
import arrow from '../../img/arrow-outlined-crimson.png';
const Slider = ({ images }) => {
let index = 0;
const nextSlide = () => {
index+=1;
if (index > images.length - 1) {
index = 0
}
}
return (
<div className='slider'>
<div ref={sliderRef} className="wrapper">
<img src={images[index]} alt="" className="sliderImg" />
</div>
<img src={arrow} onClick={nextSlide} alt="Arrow" className="sliderIcon iconRight" />
</div>
)
}
export default Slider
Use React State instead of just variables.
Try this:
const [index, setIndex] = React.useState(0)
const nextSlide = () => {
setIndex(index + 1);
if (index > images.length - 1) {
setIndex(1)
}
}

Problem with setting state.index on click

I'm creating a webpage slide show. I created a file called SlideshowData.js where I exported an array of objects with all image links and ids. So using react I mapped through the photos and added dots to them. I created the dots and each time the photos switch the dot changes color from gray to black to indicate that the current photo is active.
The problem occurs now that I keep trying to figure out how to make them clickable. So for example you would click on the first dot it would bring you to the first slide. I tried to do it with a "set.state" function and set the initial index to the "event.target.key" which I assigned slide.id but it doesn't work.
Thank you for your time. I attached the code below.
class Slideshow extends React.Component{
constructor(props){
super(props);
this.state = {
index: 0,
delay: 5000,
length: SlideshowData.length,
}
this.clickedDot = this.clickedDot.bind(this)
}
componentDidMount(){
document.addEventListener("onClick", this.clickedDot)
if(this.state.index === this.state.length -1){
setTimeout(()=> this.setState(()=>({
index: 0
})),this.state.delay)
}else{
setTimeout(()=> this.setState((state)=>({
index: state.index + 1,
})),this.state.delay)
}
}
componentDidUpdate(){
document.addEventListener("onClick", this.clickedDot)
if(this.state.index === this.state.length -1){
setTimeout(()=> this.setState(()=>({
index: 0
})),this.state.delay)
}else{
setTimeout(()=> this.setState((state)=>({
index: state.index + 1,
})),this.state.delay)
}
}
clickedDot(){
this.setState((slide) =>({
index: this.state.length
}))
console.log(this.state.index)
}
render(){
return(
<div className="slidesContainer">
<div className="SlideshowSlider" style={{ transform: `translate3d(${-this.state.index * 100}%, 0, 0)` }}>
{SlideshowData.map((slide,index) => (
<img className="slides" src={slide.image} key={index} alt="travel"/>
))}
</div>
<div className = "slideshowDots">
{SlideshowData.map((slide,idx) => (
<div onClick={this.clickedDot} key ={slide.id} className={`slideDot${slide.id === this.state.index ? " active" : ""}`} ></div>
))}
</div>
</div>
)
}
}
export default Slideshow
You can pass the index inside your onClick function on the dots such as this:
<div onClick={(idx) => this.clickedDot(idx)} key ={slide.id} className={`slideDot${slide.id === this.state.index ? " active" : ""}`} >
and then use the index to change the slide accordingly in your clickedDot function.

React image slider with dynamic data, first Image coming after clickEvent

I've built a grid of images coming from an API, and now I am trying to build a slider.
First image to be displayed should be the one clicked in the Grid below the slider , and when clicked next/prev go to the nex/prev img in the array.
How could I get it?
I am trying by setting the initial state of ``ìmgToShow```to the img clicked, but i get nothing in console. The idea would be to get that, and then , i still dont know how, update the state to show the next img in the Array.
album is the array of objects with all the images. Fetched in my Context component
clickedImagecontains the clicked image's URL. I get it from a click event in other component.
This is my component :
import React, { useContext, useState } from "react";
import { AlbumContext } from "../../context/AlbumContext";
import "./carousel.css";
import BtnSlider from "./BtnSlider";
function Carousel() {
const { clickedImage, albums } = useContext(AlbumContext);
const [imgToShow, setImgToShow] = useState(clickedImage);
console.log("imgTOSHOW", imgToShow); // empty in console
console.log("clickedIMG in context", clickedImage); // gives me the URL clicked
const [slideIndex, setSlideIndex] = useState(1);
const nextSlide = () => {
if (slideIndex !== albums.length) {
setSlideIndex(slideIndex + 1);
} else if (slideIndex === albums.length) {
setSlideIndex(1);
}
};
const prevSlide = () => {};
return (
<div className="container-slider">
{albums &&
albums.map((item, index) => {
return (
<div
key={item.id}
className={
slideIndex === index + 1 ? "slide active-anim" : "slide"
}
>
<img src={imgToShow} alt="" />
</div>
);
})}
<BtnSlider moveSlide={nextSlide} direction={"next"} />
<BtnSlider moveSlide={prevSlide} direction={"prev"} />
</div>
);
}
export default Carousel

Div onClick function doesn't work when clicking in React app

I have a function which creates 2 divs when changing the number of items correspondingly (say if we choose 5 TVs we will get 5 divs for choosing options). They serve to make a choice - only one of two possible options should be chosen for every TV, so when we click on one of them, it should change its border and background color.
Now I want to create a dynamic stylization for these divs: when we click on them, they should get a new class (tv-option-active) to change their styles.
For that purposes I used 2 arrays (classesLess and classesOver), and every time we click on one of divs we should remove a class if it's already applied to the opposite option and push the class to the target element - thus only one of options will have tv-option-active class.
But when I click on a div I do not get anything - when I open the document in the browser and inspect the elements, the elements do not even receive new class on click - however, when I console log the classes variable that should apply to an element, it is the way it should be - "less tv-option-active" or "over tv-option-active". I applied join method to remove the comma.
I checked the name of imported css file and it is ok so the problem is not there, also I applied some rules just to make sure the problem is not there and it worked when it's not dynamic (I mean no clicks are needed).
So my list of reasons causing that trouble seems to be not working.
I also tried to reorganize the code in order to not call a function in render return - putting mapping directly to render return, but this also didn't work.
Can anyone give me a hint why it is that?
Here is my code.
import React from 'react'
import { NavLink, withRouter } from 'react-router-dom'
import './TVMonting.css'
import PageTitle from '../../PageTitle/PageTitle'
class TVMontingRender extends React.Component {
state = {
tvData: {
tvs: 1,
under: 0,
over: 0,
},
}
render() {
let classesLess = ['less']
let classesOver = ['over']
const tvHandlers = {
tvs: {
decrease: () => {
if (this.state.tvData.tvs > 1) {
let tvData = {
...this.state.tvData,
}
tvData.tvs -= 1
this.setState({ tvData })
}
},
increase: () => {
if (this.state.tvData.tvs < 5) {
let tvData = {
...this.state.tvData,
}
tvData.tvs += 1
this.setState({ tvData })
}
},
},
}
const createDivs = () => {
const divsNumber = this.state.tvData.tvs
let divsArray = []
for (let i = 0; i < divsNumber; i++) {
divsArray.push(i)
}
return divsArray.map((i) => {
return (
<React.Fragment key={i}>
<div
className={classesLess.join(
' '
)}
onClick={() => {
const idx = classesOver.indexOf(
'tv-option-active'
)
if (idx !== -1) {
classesLess.splice(
idx,
1
)
}
classesLess.push(
'tv-option-active'
)
}}
>
Under 65
</div>
<div
className={classesOver.join(
' '
)}
onClick={() => {
const idx = classesLess.indexOf(
'tv-option-active'
)
if (idx !== -1) {
classesOver.splice(
idx,
1
)
}
classesOver.push(
'tv-option-active'
)
// classesOver.join(' ')
}}
>
Over 65
</div>
</React.Fragment>
)
})
}
return (
<div>
<button onClick={tvHandlers.tvs.decrease}>
-
</button>
{this.state.tvData.tvs === 1 ? (
<h1> {this.state.tvData.tvs} TV </h1>
) : (
<h1> {this.state.tvData.tvs} TVs </h1>
)}
<button onClick={tvHandlers.tvs.increase}>
+
</button>
{createDivs()}
</div>
)
}
}
export default withRouter(TVMontingRender)
CSS file is very simple - it just adds a border.
P.S. I know that with this architecture when I click on one of the divs all the divs will get tv-option-active class, but for now I just want to make sure that this architecture works - since I'm relatively new in React 🙂Thanks in advance!
Components won't have their lifecycle triggered if you are mutating a variable. You need a state for that purpose, which stores the handled data.
In your case you need some state to say which div has the active class, under or over. You can also abstract each rendered Tv to another Class component. This way you achieve independent elements that control their own class, rather than changing all others.
For that I created a Tv class, where I simplified some of the logic:
class Tv extends React.Component {
state = {
activeGroup: null
}
// this will update which group is active
changeActiveGroup = (activeGroup) => this.setState({activeGroup})
// activeClass will return 'tv-option-active' if that group is active
activeClass = (group) => (group === this.state.activeGroup ? 'tv-option-active' : '')
render () {
return (
<React.Fragment>
<div
className={`less ${ activeClass('under') }`}
onClick={() => changeActiveGroup('under')}
>
Under 65
</div>
<div
className={`over ${ activeClass('over') }`}
onClick={() => changeActiveGroup('over')}
>
Over 65
</div>
</React.Fragment>
)
}
}
Your TvMontingRender will be cleaner, also it's better to declare your methods at your class body rather than inside of render function:
class TVMontingRender extends React.Component {
state = {
tvData: {
tvs: 1,
under: 0,
over: 0,
}
}
decreaseTvs = () => {
if (this.state.tvData.tvs > 1) {
let tvData = {
...this.state.tvData,
}
tvData.tvs -= 1
this.setState({ tvData })
}
}
increaseTvs = () => {
if (this.state.tvData.tvs < 5) {
let tvData = {
...this.state.tvData,
}
tvData.tvs += 1
this.setState({ tvData })
}
}
createDivs = () => {
const divsNumber = this.state.tvData.tvs
let divsArray = []
for (let i = 0; i < divsNumber; i++) {
divsArray.push(i)
}
// it would be better that key would have an unique generated id (you could use uuid lib for that)
return divsArray.map((i) => <Tv key={i} />)
}
render() {
return (
<div>
<button onClick={this.decreaseTvs}>
-
</button>
{this.state.tvData.tvs === 1 ? (
<h1> {this.state.tvData.tvs} TV </h1>
) : (
<h1> {this.state.tvData.tvs} TVs </h1>
)}
<button onClick={this.increaseTvs}>
+
</button>
{this.createDivs()}
</div>
)
}
}
Note: I didn't change the key you are passing to Tv, but when handling an array that you manipulate somehow it's often better to pass an unique id identifier. There are some libs for that like uuid, nanoID.
When handling complex class logic, you may consider using libs like classnames, that would make your life easier.

Categories