useEffect() with modal navigation index issue - javascript

I have a component that displays a grid with images. Upon clicking a specific image, a modal pops up with buttons for navigation on either side of the image. To display the images, i map through the docs array and access the doc.url. There is also an onClick which passes the doc.url and index of the item which then updates the state of selectedImg. Here is the ImageGrid.js component code:
const ImageGrid = ({ setSelectedImg, imageIndex, setImageIndex }) => {
const { docs } = useFirestore('images');
console.log('current docs', docs)
const clickHandler = (url, index) => {
setSelectedImg(url);
setImageIndex(index);
}
return (
<div className='img-grid'>
{ docs && docs.map((doc, index) => (
<motion.div className='img-wrap' key={doc.id}
layout
whileHover={{ opacity: 1 }}
whileTap={{ scale: 0.9 }}
onClick={() => clickHandler(doc.url, index)}
>
<motion.img src={doc.url} alt='uploaded pic'
initial={{ opacity: 0 }}
animate={{ opacity: 1}}
transition={{ delay: 1 }}
/>
</motion.div>
)) }
</div>
)
}
I then have a Modal.js component with two onClick functions for previous and next buttons next to the modal image. See as follows:
const Modal = ({ selectedImg, setSelectedImg, imageIndex, setImageIndex }) => {
const { docs } = useFirestore('images');
const handleClick = (e) => {
if(e.target.classList.contains('backdrop'))
setSelectedImg(null);
}
// Navigation
const prevImage = () => {
setImageIndex(prev => prev - 1);
setSelectedImg(docs[imageIndex].url);
}
const nextImage = () => {
setImageIndex(prev => prev + 1);
setSelectedImg(docs[imageIndex].url);
}
return (
<motion.div className='backdrop' onClick={handleClick}
initial={{ opacity: 0}}
animate={{ opacity: 1}}
>
<FaChevronCircleLeft className='modal-navigation left' onClick={prevImage} />
<motion.img src={selectedImg} alt='enlarged pic'
initial={{ y: '-100vh'}}
animate={{ y: 0 }}
/>
<FaChevronCircleRight className='modal-navigation right' onClick={nextImage}/>
</motion.div>
)
}
This does work but the problem is that once you select an image from the grid and the modal appears for the first time, if you click either navigation button, nothing happens. Then on the second click, it starts to work. From my understanding, this is because the imageIndex state is updating after the button has been clicked, thefore useEffect needs to be used.
I tried to use useEffect in Modal.js where it would only work if [imageIndex] was a dependency. I kept receiving an error because setImageIndex was being used even before the Modal navigation was used.
// Navigation
const prevImage = () => {
setImageIndex(prev => prev - 1);
}
const nextImage = () => {
setImageIndex(prev => prev + 1);
}
useEffect(() => {
setSelectedImg(docs[imageIndex].url);
}, [imageIndex])
How can I pass the index of the current image and have it correspond with the navigation buttons when they're clicked?
Thanks in advance.

I worked it out.
In the Modal.js, I added another piece of state for when the navigation button is clicked. It starts off as false and when the left or right arrow is clicked, the onClick function sets the buttonClicked state to true; The onClick also sets the imageIndex - 1 or + 1 using the previous props.
Then, i added a useEffect function that basically said, every time the button was clicked, setSelectedImg to the image with the new imageIndex. And then set the buttonClicked back to false. [buttonClicked] is the dependency. See code:
const [ buttonClicked, setButtonClicked ] = useState(false);
const handleClick = (e) => {
if(e.target.classList.contains('backdrop'))
setSelectedImg(null);
}
// Navigation
const prevImage = () => {
setImageIndex(prev => prev - 1);
setButtonClicked(true);
}
const nextImage = () => {
setImageIndex(prev => prev + 1);
setButtonClicked(true);
}
useEffect(() => {
if (buttonClicked){
setSelectedImg(docs[imageIndex].url)
setButtonClicked(false);
};
}, [buttonClicked])
With the click event handlers, no need to use if statements for imageIndex arriving at a negative number or more than the amount of images in the array.
I just used inline if statements to show or hide the buttons:
{ imageIndex !== 0 && <FaChevronCircleLeft className='modal-navigation left' onClick={prevImage} /> }
{ imageIndex !== docs.length - 1 && <FaChevronCircleRight className='modal-navigation right' onClick={nextImage}/> }

Related

How to change slidesPerview on button click and screen size change with Swiper.js and React?

I want to implement a button that can change the number of slidesPerView. A screenshot of the layout:
For example, when I click 5, the number of slidesPerView changes to 5. How can I achieve that?
I've tried to implement it this way, but it didn't work:
const slidesPerViewOptions = [3, 5, 7]
const changeSlidesPerView = (option: number) => {
swiperRef.params.slidesPerView = option
}
<div>
{slidesPerViewOptions.map((option) => (
<button
key={option}
onClick={() => changeSlidesPerView(option)}
>
{option}
</button>
))}
</div>
For a proper re-render, I would use a state called slidePerview, for example. And here is an example of a setup:
const [slidePerview, setSlidePerview] = useState(1);
const changeSlidesPerView = (option) => {
setSlidePerview(option);
};
<Swiper
slidesPerView={slidePerview}
....
And if you want to also update it according to screen size change,
useEffect(() => {
const checkScreenSize = () => {
if (window.innerWidth < 750) {
setSlidePerview(1);
} else if (window.innerWidth < 1080) {
setSlidePerview(3);
} else {
setSlidePerview(5);
}
};
window.addEventListener("resize", checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize);
}, []);

React Native: Fade through a series of images using Animated & opacity

I'm currently trying to fade through a series of images. Basically, I always want to display one image at a time, then animate its opacity from 1 to 0 and that of the next image in the series from 0 to 1, and so on. Basically something like this, which I've already implemented for the web in ReactJS and CSS animations:
However, I seem to keep getting stuck on using React Native's Animated library and refs. I've tried storing the opacity of all the images in an array which itself is contained in an useRef hook. Then, using Animated, I'm trying to perform two parallel animations which change the opacity of the current image index and that of the next index. This is what I've come up with:
export default function StartImageSwitcher() {
const images = Object.values(Images).map((img) => img.imageNoShadow);
const [currentImage, setCurrentImage] = useState(0);
const opacity = useRef<Animated.Value[]>([
new Animated.Value(1),
...Array(images.length - 1).fill(new Animated.Value(0)),
]).current;
useEffect(() => {
let nextImage = currentImage + 1;
if (nextImage >= images.length) nextImage = 0;
Animated.parallel([
Animated.timing(
opacity[currentImage],
{
toValue: 0,
duration: 2000,
useNativeDriver: true,
},
),
Animated.timing(
opacity[nextImage],
{
toValue: 1,
duration: 2000,
useNativeDriver: true,
},
),
]).start(() => {
setCurrentImage(nextImage);
});
}, [currentImage]);
images.map((image, index) => console.log(index, opacity[index]));
return (
<View style={styles.imageWrapper}>
{
images.map((image, index) => (
<Animated.Image style={{ ...styles.image, opacity: opacity[index] }} source={image} key={index} />
))
}
</View>
);
}
However, this doesn't seem to work at all. When mounted, it only shows the first image, then fades that one out and all the other images in and gets stuck there:
Anyone got an idea where I messed up? I feel like I'm not using the useRef() hook in combination with the Animated library like I'm supposed to.
Your solution is pretty clever, and it generally looks like it should work to me. Performance might take a hit though as the number of images increases. I thought of an alternate method: partition the images array and then use setInterval to alternate between two Animated.Images, which get their source from each array.
// from https://stackoverflow.com/questions/41932345/get-current-value-of-animated-value-react-native
const getAnimatedValue = (value: Animated.Value) => Number.parseInt(JSON.stringify(value));
export default function StartImageSwitcher() {
// partition images into two arrays
const images1 = [];
const images2 = [];
Object.values(Images).forEach((img, i) => {
(i % 1 === 0 ? images1 : images2).push(img.imageNoShadow)
});
// use refs for the indexes so values don't become stale in the setInterval closure
const images1Index = useRef(0);
const images2Index = useRef(0);
const image1Opacity = useRef(new Animated.Value(1)).current;
const image2Opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
const swapImageInterval = setInterval(() => {
const newImage1Opacity = getAnimatedValue(image1Opacity) === 1 ? 0 : 1;
const newImage2Opacity = getAnimatedValue(image2Opacity) === 1 ? 0 : 1;
Animated.parallel([
Animated.timing(image1Opacity, {
toValue: newImage1Opacity,
duration: 2000,
useNativeDriver: true,
}),
Animated.timing(image2Opacity, {
toValue: newImage2Opacity,
duration: 2000,
useNativeDriver: true,
}),
]).start(() => {
if (newImage1Opacity === 1) {
// image 2 is now faded out, so we can swap out its source
const nextIndex = images2Index.current === images2.length - 1 ? 0 : images2Index.current + 1;
images2Index.current = nextIndex;
} else {
// image 1 is faded out, so we can swap out its source
const nextIndex = images1Index.current === images1.length - 1 ? 0 : images1Index.current + 1;
images1Index.current = nextIndex;
}
})
}, 5000)
return () => clearInterval(swapImageInterval);
}, [images1Index, images2Index]);
return (
<View style={styles.imageWrapper}>
<Animated.Image style={{ ...styles.image, opacity: image1Opacity }} source={images1[images1Index.current]} />
<Animated.Image style={{ ...styles.image, opacity: image2Opacity }} source={images2[images2Index.current]} />
</View>
);
}

Fetch all images at once and show 360 product view in Reactjs

I have already written a component that enables 360 product views for users. Users can interact with image (rotate to the right or left). This Component works well when images are from local files.
I want to fetch images from Cloudinary and render them. A product may have 100+ images so I want to show loader till all images are loaded. Once done image will render and when the user rotates new image is rendered. I was also looking for npm lib that takes care of loading and rendering images.
import React, { Component } from "react";
import "./styles.css";
import { Row, Space, Typography, Image } from "antd";
import { Md3DRotation } from "react-icons/md";
// You can play with this to adjust the sensitivity
// higher values make mouse less sensitive
const pixelsPerDegree = 1;
class React360 extends Component {
static defaultProps = { dir: "puma", numImages: 83 };
state = {
dragging: false,
imageIndex: 0,
dragStartIndex: 0,
images: [],
show360: false,
imgs:null
};
componentDidMount = () => {
document.addEventListener("mousemove", this.handleMouseMove, false);
document.addEventListener("mouseup", this.handleMouseUp, false);
// document.addEventListener("touchstart", this.handleMouseMove, false);
// document.addEventListener("touchend", this.handleMouseUp, false);
};
componentWillUnmount = () => {
document.removeEventListener("mousemove", this.handleMouseMove, false);
document.removeEventListener("mouseup", this.handleMouseUp, false);
// document.removeEventListener("touchstart", this.handleMouseMove, false);
// document.removeEventListener("touchend", this.handleMouseUp, false);
};
handleMouseDown = (e) => {
// console.log("e.screenX",Math.round(e?.touches?.[0]?.clientX))
e.persist();
this.setState((state) => ({
dragging: true,
dragStart: e.screenX || Math.round(e?.touches?.[0]?.clientX),
dragStartIndex: state.imageIndex,
}));
};
handleMouseUp = () => {
this.setState({ dragging: false });
};
updateImageIndex = (currentPosition) => {
let numImages = this.props.numImages;
const pixelsPerImage = pixelsPerDegree * (360 / numImages);
const { dragStart, imageIndex, dragStartIndex } = this.state;
// pixels moved
let dx = (currentPosition - dragStart) / pixelsPerImage;
let index = Math.floor(dx) % numImages;
if (index < 0) {
index = numImages + index - 1;
}
index = (index + dragStartIndex) % numImages;
// console.log(index, dragStartIndex, numImages)
if (index !== imageIndex) {
this.setState({ imageIndex: index === 0 ? 1 : index });
}
};
handleMouseMove = (e) => {
// console.log("handleMouseMove",this.state.dragging)
console.log("screenX", Math.round(e?.touches?.[0]?.clientX));
if (this.state.dragging) {
this.updateImageIndex(e.screenX || Math.round(e?.touches?.[0]?.clientX));
}
};
preventDragHandler = (e) => {
e.preventDefault();
};
renderImage = () => {
const { imageIndex, images } = this.state;
return (
<>
<div className="react360">
<img
alt=""
src={
require(`../../assets/puma_opti/IMG_00${
imageIndex > 9 ? imageIndex : "0" + imageIndex
}.JPG`).default
}
/>
</div>
<Row justify="center" style={{ marginTop: "1.5em" }}>
<Space>
<Md3DRotation size={25} color="#8C8C8C" />
<Typography.Title
level={5}
type="secondary"
style={{ fontFamily: "Gilroy" }}
className="helptext-360"
>
Drag to view 360 degrees
</Typography.Title>
</Space>
</Row>
</>
);
};
render = () => {
return (
<div
className="react-360-img"
onMouseDown={this.handleMouseDown}
onDragStart={this.preventDragHandler}
// onTouchStart={this.handleMouseMove}
onTouchStart={this.handleMouseDown}
onTouchMove={this.handleMouseMove}
onTouchEnd={this.handleMouseUp}
>
{this.renderImage()}
</div>
);
};
}
1.For library you can use React Loading Skeleton : https://www.npmjs.com/package/react-loading-skeleton
2.If you want to display loading when you get data you have to add condition to your jsx, for instance:
fetchData.loading?(
return <Skeleton/>
):(
<div className="react360">
data.map(res=>{
<img
alt=""
src={
require(`../../assets/puma_opti/IMG_00${
imageIndex > 9 ? imageIndex : "0" + imageIndex}.JPG`).default
}
/>
}))
</div>
N.B: fetchdata is the status of your API call (if is loading or successful), data is data obtained from API.

React Carousel rotate on delay issue when setting state in parent component causing rerender?

Hopefully, the title isn't too ambiguous, but I am struggling how to summarise my issue in just a few words.
What I am attempting to achieve is the one image with appear for 1.5 seconds, the opacity of image will go from 0.3 to 1 and a string of text "Completed" appears below the image, and will then rotate and move to the next image where it will have the same outcome, constantly looping through each image.
The issue is, it only seems to rotate once. It will update the styles and add the text below the image but fail to rotate.
I have a 3d carousel component that rotates the items within an array logos when a function is called rotate().
const [rotateDeg, setRotateDeg] = useState(0);
const [currentIndex, setCurrentIndex] = useState(0);
const rotate = () => {
const maxIndex = logos.length - 1;
const incrementIndex = currentIndex + 1;
const newIndex = incrementIndex > maxIndex ? 0 : incrementIndex;
setCurrentIndex(newIndex);
setRotateDeg(rotateDeg - 360 / logos.length);
};
return (
<Container>
<Carousel ref={carousel} logosLength={logos.length} rotateDeg={rotateDeg}>
{logos.map((item, index) => {
const { key } = item;
return (
<Item
key={key}
index={index}
logosLength={logos.length}
currentIndex={currentIndex}
>
<LoadingSpinLogo
item={item}
delay={1500 * (index + 1)}
rotate={() => rotate()}
key={key}
isCurrent={currentIndex === index}
/>
</Item>
);
})}
</Carousel>
</Container>
);
The component LoadingSpinLogo is where I seem to be having an issue.
I have tried to pass different dependencies into the useEffect callback but it seems to cause weird issues with the delay.
const LoadingSpinLogo = ({ item, rotate, delay, isCurrent }) => {
const [completed, setCompleted] = useState(false);
const [divStyle, setDivStyle] = useState({ opacity: 0.3 });
const props = useSpring(divStyle);
const customStyles = {
height: "78px",
width: "115px",
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
display: "block",
margin: "auto"
};
const updateCompleted = () => {
setCompleted(true);
setDivStyle({ opacity: 1, from: { opacity: 0.3 } });
rotate();
};
useEffect(() => {
const timeoutID = setTimeout(function() {
updateCompleted();
}, delay);
return () => {
// Clean up the subscription
window.clearInterval(timeoutID);
};
}, []);
return (
<LoadingSpinContainer>
<animated.div style={props}>
<ImageServer png={item.url} customStyles={customStyles} />
</animated.div>
{completed ? "Completed" : null}
</LoadingSpinContainer>
);
};
Here is a CodeSanbox of my components.
Any idea where I am going wrong here?
Any help would be greatly appreciated.
Removing the [] in your useEffect function seems to works fine for me
useEffect(() => {
const timeoutID = setTimeout(function() {
updateCompleted();
}, delay);
return () => {
// Clean up the subscription
window.clearInterval(timeoutID);
};
}, );

React Native how to use FlatList inside nativebase Tabs onEndReach keep fire non-stop

I'm new to react native so i use some component from 3rd party library and try to use react native component as possible.
ReactNative: 0.54
NativeBase: 2.3.10
....
i had problem with FlatList inside Tabs from Nativebase base on scrollView
onEndReachedThreshold not working correctly as Doc say 0.5 will trigger haft way scroll of item but when i set 0.5 it not trigger haft way to last item it wait until scroll to last item and it trigger onEndReach.
i had problem with onEndReach if i use ListFooterComponent to render loading when data not delivery it keep firing onEndReach non-stop.
here is my code
check props and init state
static getDerivedStateFromProps(nextProps) {
const { params } = nextProps.navigation.state;
const getCategoryId = params ? params.categoryId : 7;
const getCategoryIndex = params ? params.categoryIndex : 0;
return {
categoryId: getCategoryId,
categoryIndex: getCategoryIndex,
};
}
state = {
loadCategoryTab: { data: [] },
loadProduct: {},
storeExistId: [],
loading: false,
refreshing: false,
}
loadCategory
componentDidMount() { this.onLoadCategory(); }
onLoadCategory = () => {
axios.get(CATEGORY_API)
.then((res) => {
this.setState({ loadCategoryTab: res.data }, () => {
setTimeout(() => { this.tabIndex.goToPage(this.state.categoryIndex); });
});
}).catch(error => console.log(error));
}
Check onChange event when Tabs is swip or click
onScrollChange = () => {
const targetId = this.tabClick.props.id;
this.setState({ categoryId: targetId });
if (this.state.storeExistId.indexOf(targetId) === -1) {
this.loadProductItem(targetId);
}
}
loadProductItem = (id) => {
axios.get(`${PRODUCT_API}/${id}`)
.then((res) => {
/*
const {
current_page,
last_page,
next_page_url,
} = res.data;
*/
this.setState({
loadProduct: { ...this.state.loadProduct, [id]: res.data },
storeExistId: this.state.storeExistId.concat(id),
});
})
.catch(error => console.log(error));
}
loadMoreProduct when onEndReach is trigger
loadMoreProductItem = () => {
const { categoryId } = this.state;
const product = has.call(this.state.loadProduct, categoryId)
&& this.state.loadProduct[categoryId];
if (product.current_page !== product.last_page) {
axios.get(product.next_page_url)
.then((res) => {
const {
data,
current_page,
last_page,
next_page_url,
} = res.data;
const loadProduct = { ...this.state.loadProduct };
loadProduct[categoryId].data = product.data.concat(data);
loadProduct[categoryId].current_page = current_page;
loadProduct[categoryId].last_page = last_page;
loadProduct[categoryId].next_page_url = next_page_url;
this.setState({ loadProduct, loading: !this.state.loading });
}).catch(error => console.log(error));
} else {
this.setState({ loading: !this.state.loading });
}
}
render()
render() {
const { loadCategoryTab, loadProduct } = this.state;
const { navigation } = this.props;
return (
<Container>
<Tabs
// NB 2.3.10 not fix yet need to use `ref` to replace `initialPage`
ref={(component) => { this.tabIndex = component; }}
// initialPage={categoryIndex}
renderTabBar={() => <ScrollableTab tabsContainerStyle={styles.tabBackground} />}
onChangeTab={this.onScrollChange}
// tabBarUnderlineStyle={{ borderBottomWidth: 2 }}
>
{
loadCategoryTab.data.length > 0 &&
loadCategoryTab.data.map((parentItem) => {
const { id, name } = parentItem;
const dataItem = has.call(loadProduct, id) ? loadProduct[id].data : [];
return (
<Tab
key={id}
id={id}
ref={(tabClick) => { this.tabClick = tabClick; }}
heading={name}
tabStyle={styles.tabBackground}
activeTabStyle={styles.tabBackground}
textStyle={{ color: '#e1e4e8' }}
activeTextStyle={{ color: '#fff' }}
>
<FlatList
data={dataItem}
keyExtractor={subItem => String(subItem.prod_id)}
ListEmptyComponent={this.onFirstLoad}
// ListFooterComponent={this.onFooterLoad}
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
onEndReachedThreshold={0.5}
onEndReached={() => {
this.setState({ loading: !this.state.loading }, this.loadMoreProductItem);
}}
renderItem={({ item }) => {
const productItems = {
item,
navigation,
};
return (
<ProductItems {...productItems} />
);
}}
/>
// this OnLoadFooter is my tempory show loading without ListFooterComponent but i don't want to show loading outside FlatList hope i will get a help soon
<OnLoadFooter loading={this.state.loading} style={{ backgroundColor: '#fff' }} />
</Tab>
);
})
}
</Tabs>
</Container>
);
}
Loading Component
function OnLoadFooter(props) {
if (props.loading) return <Spinner style={{ height: 50, paddingVertical: 10 }} />;
return null;
}
Let me explain my process
init CategoryId and CategoIndex for Tabs active
after axios fire will get all category and render Tab item because nativebase Tabs bug when initailPage bigger than 0 it show blank page and i use ref trigger it when category complete load when this.tabIndex.goToPage is trigger it call onChange
onChage event start to check if tabClick Ref exist in StoreExistId that save category when they click if true we load product else we do nothing. i need ref in this because React state is async making my product fire loading duplicate data for 1st time so Ref come in to fix this.
when scroll down to last item it will loadMoreProduct by paginate on API
my data in state like below
StoreExistId: [1,2,3,4]
loadProduct: {
1: {data: [.....]},
2: {data: [.....]},
etc....
}
Thank in advanced
Some of NativeBase components use scrollView inside. I guess it could be ScrollableTab component which uses ScrollView? You should not use FlatList inside ScrollView, onReachEnd will not work then.
I was facing the same problem, solution is to use <FlatList> inside <Content> . For more information see https://stackoverflow.com/a/54305517/8858217

Categories

Resources