Matching setState interval with css animation - javascript

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?

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.

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>
);

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

Related

React setTimeout inside useLayoutEffect not delaying animation if the timeout duration is too short

I'm trying to create an animation, where the text appears in the screen and have the option to delay the animation using a timeout.
The strange thing is, when the app first render on the browser, it works as intended, but if I manually refresh "f5", the delay is only visible if it's a longer timeout duration, any value lower than 500ms for example, the others the delay is no visible.
I've already tried to create a custom hook, use "useEffect" and "useLayoutEffect" but this strange behavior keep happening.
Here is the component.
const FadeInText = ({text, style, timeout, noScroll}) => {
const textRef = useRef(null)
const [visible, setVisible] = useState()
useLayoutEffect(() => {
setVisible(timeout !== undefined ? {animation: "none",
opacity: 0} : {})
let time = setTimeout(() => {
setVisible({})
}, timeout)
return () => clearTimeout(time)
},[timeout])
return <p ref={textRef} style={{...visible,...style}} className="fade-in-text">{text}</p>
}
export default FadeInText;
Here are the CSS rules applied to the text
#keyframes fade-slide-up {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-text {
font-size: 1rem;
color: white;
animation: fade-slide-up 0.2s ease-in;
}
Here are some cases where I use this component, the only component where the delay is visible, is the one with the timeout set as 600
<FadeInText style={{fontSize: "5vw"}} text="R" timeout={200} />
<FadeInText style={{fontSize: "5vw"}} text="A" timeout={300} />
<FadeInText style={{fontSize: "5vw"}} text="N" timeout={400} />
<FadeInText style={{fontSize: "5vw"}} text="D" timeout={600} />
Use clearTimeout instead of clearInterval.
Make sure timeout value is in milliseconds (1000, 2000, 3500 ...)
Without css rules applied or value of variables, it's hard to help.

Image loading in react "with refs" vs "with state"

What I want to do is, I've an Image component which shows some loading state as placeholder colour (via css background-colour) till image loads then swap it with the actual image.
Css
.show-img {
opacity: 1;
transition: all 0.3s ease-in;
}
.hide-img {
background-color:#eee;
opacity: 0;
transition: all 0.3s ease-in;
}
Using state
const Image = () => {
const [loading, setLoading] = useState(false);
const onImgLoad = () => {
setLoading(true);
};
return (
<img
src="https://i.picsum.photos/id/498/200/300.jpg"
className={loading ? 'hide-img' : 'show-img'}
onLoad={onImgLoad}
/>
);
};
Using refs
const Image = () => {
const ref = useRef();
const onImgLoad = (e) => {
//if img is loaded
if (e.target.src && ref.current) {
ref.current?.classList?.remove('hide-img');
ref.current?.classList?.add('show-img');
}
};
return (
<img
ref={ref}
src="https://i.picsum.photos/id/498/200/300.jpg"
className="hide-img"
onLoad={onImgLoad}
/>
);
};
I want to know which one of the approach is more performant and why? I was thinking about avoiding re-rendering due to state update for such a basic task (maybe).
PS: I've this Image component inside the carousel and there are multiple carousels on the page.
Thank you,
Theoretically speaking, I would argue that using state is more performant as the state is handled by react and its virtual DOM which is way faster than updating the DOM directly with useRef. Additionally, using state warranties the component re-rendering but optimised by react, which means only when its needed. I used the following articles to reach to this conclusion.
https://medium.com/swlh/useref-explained-76c1151658e8
https://blog.logrocket.com/usestate-vs-useref/
Nonetheless you may have to implement a profiler to check if this is true, as for the final user it may not look as the fastest solution, due to other circumstances outside react, like the amount of images, the connection speed and whether the images are in a CDN or not, just to mention some.

Can I wait for multiple CSS animations using JS?

We have a way to detect when an animation ends using JS:
const element = $('#animatable');
element.addClass('being-animated').on("animationend", (event) => {
console.log('Animation ended!');
});
#keyframes animateOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#keyframes animatePosition {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 15px, 0);
}
}
#animatable.being-animated {
animation: animateOpacity 1s ease 0s forwards, animatePosition 2s ease 0s forwards;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="animatable">I'm probably being animated.</div>
And as you can see, JS, rightfully, because I'm hooked to the animationend event tells me "yup, an animation is done" but isn't aware of what's coming after and I'm missing the second one.
Isn't there an animation queue? Surely CSS has to register these things somewhere in the system before they're fired and I could peak inside.
Disclaimer: I don't think jQuery is important to answer this question and would hurt both load and runtime performance if others choose to rely on this code after seeing this answer. So, I will be answering with vanilla JavaScript to help as many people as I can with this, but if you want to use jQuery, you can still apply the same concepts.
Answer: There isn't an animation queue, but you could make your own.
For example, you could link data about animations to your target element using a closure, and/or a Map (In the snippet below, I actually used a WeakMap in an attempt to help garbage collection). If you save animation states as true when they are completed, you could check and eventually fire a different callback when all are true, or dispatch a custom event of your own. I used the custom event approach, because it's more flexible (able to add multiple callbacks).
The following code should additionally help you avoid waiting for ALL animations in those cases where you only actually care about a couple specific ones. It should also let you handle animation events multiple times and for multiple individual elements (try running the snippet and clicking the boxes a few times)
const addAnimationEndAllEvent = (() => {
const weakMap = new WeakMap()
const initAnimationsObject = (element, expectedAnimations, eventName) => {
const events = weakMap.get(element)
const animationsCompleted = {}
for (const animation of expectedAnimations) {
animationsCompleted[animation] = false
}
events[eventName] = animationsCompleted
}
return (element, expectedAnimations, eventName = 'animationendall') => {
if (!weakMap.has(element)) weakMap.set(element, {})
if (expectedAnimations) {
initAnimationsObject(element, expectedAnimations, eventName)
}
// When any animation completes...
element.addEventListener('animationend', ({ target, animationName }) => {
const events = weakMap.get(target)
// Use all animations, if there were none provided earlier
if (!events[eventName]) {
initAnimationsObject(target, window.getComputedStyle(target).animationName.split(', '), eventName)
}
const animationsCompleted = events[eventName]
// Ensure this animation should be tracked
if (!(animationName in animationsCompleted)) return
// Mark the current animation as complete (true)
animationsCompleted[animationName] = true
// If every animation is now completed...
if (Object.values(animationsCompleted).every(
isCompleted => isCompleted === true
)) {
const animations = Object.keys(animationsCompleted)
// Fire the event
target.dispatchEvent(new CustomEvent(eventName, {
detail: { target, animations },
}))
// Reset for next time - set all animations to not complete (false)
initAnimationsObject(target, animations, eventName)
}
})
}
})()
const toggleAnimation = ({ target }) => {
target.classList.toggle('being-animated')
}
document.querySelectorAll('.animatable').forEach(element => {
// Wait for all animations before firing the default event "animationendall"
addAnimationEndAllEvent(element)
// Wait for the provided animations before firing the event "animationend2"
addAnimationEndAllEvent(element, [
'animateOpacity',
'animatePosition'
], 'animationend2')
// Listen for our added "animationendall" event
element.addEventListener('animationendall', ({detail: { target, animations }}) => {
console.log(`Animations: ${animations.join(', ')} - Complete`)
})
// Listen for our added "animationend2" event
element.addEventListener('animationend2', ({detail: { target, animations }}) => {
console.log(`Animations: ${animations.join(', ')} - Complete`)
})
// Just updated this to function on click, so we can test animation multiple times
element.addEventListener('click', toggleAnimation)
})
.animatable {
margin: 5px;
width: 100px;
height: 100px;
background: black;
}
#keyframes animateOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#keyframes animatePosition {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 15px, 0);
}
}
#keyframes animateRotation {
100% {
transform: rotate(360deg);
}
}
.animatable.being-animated {
animation:
animateOpacity 1s ease 0s forwards,
animatePosition 1.5s ease 0s forwards,
animateRotation 2s ease 0s forwards;
}
<div class="animatable"></div>
<div class="animatable"></div>
#BDawg's awesome snippet is more flexible and thorough, it certainly deserves to be the accepted answer. That said, I was inspired to see if a less verbose approach was feasible. Here's what I came up with.
It's pretty self-explanitory, but basically the concept is that all the animation properties' indexes correlate, and we can use that to find the name of the animation that finishes last.
const getFinalAnimationName = el => {
const style = window.getComputedStyle(el)
// get the combined duration of all timing properties
const [durations, iterations, delays] = ['Duration', 'IterationCount', 'Delay']
.map(prop => style[`animation${prop}`].split(', ')
.map(val => Number(val.replace(/[^0-9\.]/g, ''))))
const combinedDurations = durations.map((duration, idx) =>
duration * iterations[idx] + delays[idx])
// use the index of the longest duration to select the animation name
const finalAnimationIdx = combinedDurations
.findIndex(d => d === Math.max(...combinedDurations))
return style.animationName.split(', ')[finalAnimationIdx]
}
// pipe your element through this function to give it the ability to dispatch the 'animationendall' event
const addAnimationEndAllEvent = el => {
const animationendall = new CustomEvent('animationendall')
el.addEventListener('animationend', ({animationName}) =>
animationName === getFinalAnimationName(el) &&
el.dispatchEvent(animationendall))
return el
}
// example usage
const animatable = document.querySelector('.animatable')
addAnimationEndAllEvent(animatable)
.addEventListener('animationendall', () => console.log('All animations have finished'))
.animatable {
width: 50px;
height: 50px;
background-color: red;
position: relative;
left: 0;
animation: 1.5s slidein, 1s fadein;
}
#keyframes slidein {
0% { left: 100vw; }
100% { left: 0; }
}
#keyframes fadein {
0% { opacity: 0; }
100% { opacity: 1; }
}
<div class="animatable"></div>
First technique:
Add a class to an element, then handle every animation and wait for them to end, no matter what. This is the common way to do things where you trigger animations by classes.
As per Kaiido's comment and pointing out, this waits for every single animation, no matter how long to finish. This was the motivation behind all of this: create a nice animation and make JS aware of it (no matter how complex / long) finishing it so you could then chain other things.
If you don't do this, you might have a nice animation running and suddenly being cut by something else and...that's bad.
const triggerAnimationWithClass = (classToAdd, toWhat) => {
const element = document.querySelector(toWhat);
/**
* Initialize the count with 1, because you'll always have at least one animation no matter what.
*/
let animationCount = 1;
return new Promise((resolve, reject) => {
element.addEventListener('animationend', (event) => {
if((window.getComputedStyle(element).animationName).split(',').length - animationCount === 0) {
/**
* Remove the current function being hooked once we're done. When a class gets added that contains any N number of animations,
* we're running in a synchronous environment. There is virtually no way for another animation to happen at this point, so, we're
* surgically looking at animations that only happen when our classToAdd gets applied then hooking off to not create conflicts.
*/
element.removeEventListener('animationend', this);
const animationsDonePackage = {
'animatedWithClass': classToAdd,
'animatedElement': toWhat,
'animatedDoneTime': new Date().getTime()
};
resolve(animationsDonePackage);
} else {
animationCount++;
}
});
element.classList.add(classToAdd);
});
}
This handles multiple classes being added. Let's assume that from the outside, someone adds yet another class at the same time (weird, but, let's say it happens) you've added yours. All the animations on that element are then treated as one and the function will wait for all of them to finish.
Second technique:
Based on #B-Dawg's answer. Handle a set of animations, based on name (CSS animation names), not class, please read the after-word:
const onAnimationsComplete = ({element, animationsToLookFor}) => {
const animationsMap = new WeakMap();
if(!animationsMap.has(element)) {
const animationsCompleted = {};
for(const animation of animationsToLookFor) {
animationsCompleted[animation] = false;
}
animationsMap.set(element, animationsCompleted);
}
return new Promise((resolve, reject) => {
// When any animation completes...
element.addEventListener('animationend', ({target, animationName, elapsedTime}) => {
const animationsCompleted = animationsMap.get(target);
animationsCompleted[animationName] = true;
// If every animation is now completed...
if(Object.values(animationsCompleted).every(isCompleted => isCompleted === true)) {
const animations = Object.keys(animationsCompleted);
// Reset for next time - set all animations to not complete (false)
animations.forEach(animation => animationsCompleted[animation] = false);
//Remove the listener once we're done.
element.removeEventListener('animationend', this);
resolve({
'animationsDone': animationsToLookFor
});
}
});
});
};
This has a bug. Assuming that a new animation comes from, say, maybe a new class, if it's not put in the animationsToLookFor list, this never resolves.
Trying to fix it but if we're talking about a precise list of animations you're looking for, this is the go-to.

React: Trying to achieve a sequential fade-in effect with a Masonry layout but it's out of order

I been trying to make a Masonry gallery with a sequential fade-in effect so that the pictures fade in one by one. And there is also a shuffle feature which will randomize the images and they fade in again after being shuffled.
here is the demo and the code:
https://tuo1t.csb.app/
https://codesandbox.io/s/objective-swartz-tuo1t
When first visiting the page, the animation is correct. However once we click on the shuffle button, something weird happened: There are often some pictures don't fade-in sequentially after the image before them faded in, there is even no fade-in animation on them, they just show up out of order.
The way I achieved this animation is by adding a delay transition based on the index of the image, and use ref to track images.
first I initialize the ref
let refs = {};
for (let i = 0; i < images.length; i++) {
refs[i] = useRef(null);
}
and I render the gallery
<Mansory gap={"1em"} minWidth={minWidth}>
{imgs.map((img, i) => {
return (
<PicContainer
index={img.index}
selected={isSelected}
key={img.index}
>
<Enlarger
src={img.url}
index={img.index}
setIsSelected={setIsSelected}
onLoad={() => {
refs[i].current.toggleOpacity(1); <--- start with zero opacity images till those are loaded
}}
ref={refs[i]}
realIndex={i}
/>
</PicContainer>
);
})}
</Mansory>
for the every image component
class ZoomImg extends React.Component {
state = { zoomed: false, opacity: 0 };
toggleOpacity = o => {
console.log("here");
this.setState({ opacity: o }); <-- a setter function to change the opacity state via refs:
};
render() {
const {
realIndex,
index,
src,
enlargedSrc,
setIsSelected,
onLoad
} = this.props;
return (
<div style={{ margin: "0.25rem" }} onLoad={onLoad}>
<Image
style={{
opacity: this.state.opacity,
transition: "opacity 0.5s cubic-bezier(0.25,0.46,0.45,0.94)",
transitionDelay: `${realIndex * 0.1}s` <--- add a delay transition based on the index of the image.
}}
zoomed={this.state.zoomed}
src={src}
enlargedSrc={enlargedSrc}
onClick={() => {
this.setState({ zoomed: true });
setIsSelected(index);
}}
onRequestClose={() => {
this.setState({ zoomed: false });
setIsSelected(null);
}}
renderLoading={
<div
style={{
position: "absolute",
top: "50%",
color: "white",
left: "50%",
transform: "translateY(-50%} translateX(-50%)"
}}
>
Loading!
</div>
}
/>
</div>
);
}
}
I used console.log("here"); in the setter function, which will be called for changing the opacity state via refs. There are 16 images, so initially it is called 16 times. But when I clicked on the shuffle button, you can see that it is called fewer than 16 times because some of the pictures show up directly without fading in.
I been struggling with this problem for days and really hope someone can give me some hints.
The problem is that your are adding only some new images in the shuffle method, one approach is to apply 0 opacity to all refs first, then wait a few ms to add 1 opacity again, like here.
But, I would recommend a better approach for animation, I love shifty and its Tweenable module.

Accessing dynamic style variables (location) in react native

I'll try making my question as clear as possible.
Generally speaking, how can I get a component location attribute: left, top etc') if it changes constantly?
Specifically:
I've created a component in react-native that is animating left and top properties of his son styles (this is an the code for the component animating the left position of the object):
class MovementAnimation extends Component {
state = {
moveAnim: new Animated.Value(100), // Initial value for left: 100
}
componentDidMount() { //Call the animation
this.runMovementAnimation()
}
runMovementAnimation(){
var firstPosRand = parseInt(randomize('0', 3)) % 300;
var secondPosRand = parseInt(randomize('0', 3)) % 300;
var firstTimeRand = (parseInt(randomize('0',5)) % 3000) + 3000; //Stupid temporary solution to set a number between 500-3500
var secondTimeRand = (parseInt(randomize('0',5))% 3000) + 3000;
Animated.sequence([
Animated.timing(this.state.moveAnim, {
toValue: firstPosRand, // Animation
duration: firstTimeRand
}),
Animated.timing(this.state.moveAnim, {
toValue: secondPosRand,
duration: secondTimeRand
})
]).start(() => this.runMovementAnimation()) // repeat the animation
}
render() {
let { moveAnim } = this.state;
return (
<Animated.View ref='myElement' // Special animatable View
style={{
...this.props.style,
left: moveAnim, // Bind opacity to animated value
}}
>
{this.props.children}
</Animated.View>
);
}
}
Now, in my mainPage I'm controlling a circle component I've made with this animation component, and it works great. BUT, I want to use this specific circle 'left' attribute to control a different one, how can I access it from a different class?
Thanks in advance!
Amit

Categories

Resources