I am rotating the items in an array of images inside an interval of 5 seconds. Then I have a css animation with styled components that eases in the gallery images with a fade that occurs at the same time interval.
const fadeIn = keyframes`
5%, 95% { opacity: 1 }
100% { opacity: 0 }
`
export const Gallery = styled.div<{ lapse: number }>`
position: relative;
margin: 0 auto;
max-width: 1064px;
opacity: 0;
animation: ${fadeIn} ease-in-out ${({ lapse }) => `${lapse}s`} infinite;
`
The problem is that when I change the state
even thou at first it seems in sync, eventually the setState
takes a bit longer
import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])
// Time in seconds for each image swap, fading in between
const lapse = 5
...
useEffect(() => {
// Clone the images array
const imgs = [...images]
// Time interval same as css animation to fade in
const interval = setInterval(() => {
// Take the first element and put it at the end
imgs.push(...imgs.splice(0, 1))
// Update the state, this seems to desync as time passes
setImages(imgs)
}, lapse * 1000)
return () => clearInterval(interval)
}, [images])
return (
<Gallery lapse={lapse}>
<Portal>
<img src={imgs/${images[0]}`}
</Portal>
<Thumbnails>
<Thumbwrapper>
<img src={imgs/${images[1]}`}
</Thumbwrapper>
<Thumbwrapper>
<img src={imgs/${images[2]}`}
</Thumbwrapper>
</Thumbnails>
</Gallery>
)
Is there a way I can make sure the swapping happends smoothly?
CodePudding user response:
Make the component class based,
constructor(props) {
super(props);
this.state = {
interval: null,
images: ["1.jpg", "2.jpg", "3.jpg"],
timeChanged: null
};
}
// setInterval when component is mounted
componentDidMount() {
var interval = setInterval(()=> {
// use callback argument for setState
// when new value (images) depends on old value
this.setState((state)=>({
timeChanged: (new Date()).toString(),
images: state.images.slice(1).concat(state.images[0])
// add 1st img at the end
});
}, 3000);
this.setState({interval: interval});
}
// clearInterval before component is unmounted
componentWillUnmount() {
clearInterval(this.state.interval);
}
When the state is updated using its old value the callback argument should be used.
(See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)
slice returns a portion of an array.
Also see how to update state arrays in React.
If you still wish to use hooks. Just call setImages
with
(images)=>images.slice(1).concat(images[0])
.
I'm not sure where the clearTimeout
should be, when using hooks.
CodePudding user response:
It would be great if we can have example in codesandbox. Also I would like to ask you, maybe I am missing something, but how do you determine when to stop using effect, since it depends on image
, which is, I assume, part of the useSate hook, and in that same effect you are updating that same image
, which will lead to another effect usage(leading to infinity loop)?
UPDATED ANSWER
I made similar example on codesandbox to look it on my own. It looks to me that main problem here is sync between applied CSS and actual update in your react component. When using setInterval with 5000ms it does not mean that it will be triggered right after 5000ms, but CSS animation on the other side is always right on time, so it stuck when two of those become unsynced.
It looks like the only way to solve this is to recreate img
elements on each reorder, by introducing timestamp state and use it as part of the each img
key in order to force img
recreation on each update. Also you would need to change animation in simpler way.
CSS would besomething like this:
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.image {
position: relative;
margin: 0 auto;
max-width: 1064px;
animation: fadeIn 3s;
opacity: 1;
}
And functional component would be something like this(hook part only):
const [images, setImages] = useState(["1.jpg", "2.jpg", "3.jpg"]);
const [dateTimeWhenChanged, setDatetimeWhenChanged] = useState("");
useEffect(() => {
const imgs = [...images];
// Time interval same as css animation to fade in
const interval = setInterval(() => {
// Take the first element and but it to the end
imgs.push(...imgs.splice(0, 1));
// Update the state, this seems to desync as time passes
setImages(imgs);
setDatetimeWhenChanged(new Date().toString());
}, 5 * 1000);
return (
<div>
{images.map((img, ind) => (
<img
key={`${ind}-${dateTimeWhenChanged}`}
alt={img}
src={`images/${img}`}
className="imgg"
/>
))}
</div>
);
CodePudding user response:
Given the lack of a better proposition, I came to the conclusion that having two 'clocks' to keep the time, one being react setState and the other css animation, was at the core of the problem.
So I am now keeping a single interval with a setTimeout to make sure it goes in order then substract the time taken on the timeouts from the interval and toggle the class for css
export const Gallery = styled.div<{ fadeIn: boolean }>`
position: relative;
margin: 0 auto;
max-width: 1064px;
transition: opacity 0.3s;
opacity: ${({ fadeIn }) => (fadeIn ? 1 : 0)};
`
import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])
const [fadeIn, setFadeIn] = useState<boolean>(true)
useEffect(() => {
let interval: ReturnType<typeof setInterval>
if (images.length) {
interval = setInterval(() => {
setFadeIn(false)
setTimeout(() => {
setImages(images => images.slice(1).concat(images[0]) )
setFadeIn(true)
}, 400)
}, (lapse - 0.4) * 1000)
}
return () => clearInterval(interval)
}, [images])
const getImage = (i: number) => <img src={imgs/${images[i]}`} />
<Gallery fadeIn={fadeIn}>
<Portal>{getImage(0)}</Portal>
<Thumbnails>
<Thumbwrapper>{getImage(1)}</Thumbwrapper>
<Thumbwrapper>{getImage(2)}</Thumbwrapper>
</Thumbnails>
</Gallery>
I did however used @Nice Books better setState
aproach