I am trying to call setState({ flash: false })
followed by setState({ flash: 'up' })
which should remove the flash class from the DOM element and add it again, therefore triggering the css animation defined on that class (it only works the first time).
This is not working as expected, unless I introduce some timeouts. The first time the class is added, the animation is triggered. But subsequent calls to componentWillReceiveProps
fail to trigger the animation. I'm not sure if this is an issue with the way React updates the DOM or an inherent limitation of CSS animations. I know React has some specialised utilities for animations but I'd rather keep the code below if there's any way to fix it.
Stat.js
import React, { Component } from 'react'
import { Link } from 'react-router'
import styles from './Stat.scss'
export default class Stat extends Component {
constructor(props) {
super(props)
this.state = {
flash: false,
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.count !== this.props.count) {
const flash = nextProps.count > this.props.count
? 'Up'
: 'Down'
this.setState({ flash: false }, () => {
this.setState({ flash })
})
}
}
render() {
const { count, name, href } = this.props
const { flash } = this.state
const flashClass = flash ? styles[`flash${flash}`] : ''
return (
<Link to={href} className="stat-link">
<div className={`stat-number ${flashClass}`}>
{count}
</div>
<div className="stat-label">{name}</div>
</Link>
)
}
}
Stat.scss
$highlight-up-color: green;
$highlight-down-color: red;
@keyframes highlight-up {
0% {
color: $highlight-up-color;
}
100% {
color: default;
}
}
@keyframes highlight-down {
0% {
color: $highlight-down-color;
}
100% {
color: default;
}
}
.flashUp {
animation: highlight-up 2s;
}
.flashDown {
animation: highlight-down 2s;
}
I'm using this as a temporary fix, which kind of works except the animation is not re-triggered if another one is pending:
componentWillReceiveProps(nextProps) {
if (nextProps.count !== this.props.count) {
const flash = nextProps.count > this.props.count
? 'Up'
: 'Down'
this.setState({ flash })
if (__CLIENT__) {
window.clearTimeout(this.timeout)
this.timeout = window.setTimeout(() => {
this.setState({ flash: false })
}, 2000)
}
}
}
componentWillUnmount() {
if (__CLIENT__) {
clearTimeout(this.timeout)
}
}
@olalonde I would say it's an expected behavior since React batches updates to DOM with RAF.
You can probably use forceUpdate
for this. A better way probably would be to use componentDidUpdate
to remove the class there.
I would say it's an expected behavior since React batches updates to DOM with RAF.
This is not correct. React does not use RAF to delay applying updates.
React only batches setState
calls that happen while handling the DOM event.
@olalonde Can you create a JSFiddle demonstrating the problem?
@gaearon here: https://jsfiddle.net/v1qxfqkn/. expected behaviour is to see the background flash green whenever the count goes up and flash red whenever the count goes down.
Looks like React is processing the className change so quickly that it doesn't actually register in the DOM. The class
attribute flashes in devtools, indicating it's being set, but the class itself is never removed. As a workaround, if you add a delay to the second setState
call it does work like you're describing.
this.setState({ flash: false }, () => {
setTimeout(() => this.setState({ flash }), 0)
})
I don鈥檛 think there is anything actionable for React here. When you call setState
, React will update the DOM. It might do this asynchronously鈥攊f you call setState
from an event handler or a lifecycle hook鈥攂ut still during the same tick.
Two DOM changes in the same tick probably don鈥檛 trigger the animation. So something like
this.setState({ flash: false }, () => {
this.setState({ flash })
})
doesn鈥檛 work for this reason鈥攂oth DOM changes end up in the same tick.
For some reason it still doesn鈥檛 always animate for me even if I put a setTimeout
there. I鈥檓 not sure why but it doesn鈥檛 look related to React.
I will close this and encourage you to create a plain JS sample that demonstrates this behavior. If you can show that the issue only happens with React, I鈥檒l be happy to reopen. For now, it seems like more CSS/DOM issue. (e.g. maybe it is better to listen to onAnimationEnd
rather than timeouts鈥擨 don鈥檛 really know.)
Just found this: https://css-tricks.com/restart-css-animation/
$("#logo").click(function() {
// not gonna work
$(this).removeClass("run-animation").addClass("run-animation");
});
Apparently, this is not related to React (but it'd still be nice if someone could point out the React way to achieve what I'm trying to do).
I found the following workaround:
componentWillReceiveProps(nextProps) {
if (nextProps.count !== this.props.count) {
const flash = nextProps.count > this.props.count
? 'Up'
: 'Down'
this.setState({ flash })
}
}
render() {
const { count } = this.props
const { flash } = this.state
const flashClass = flash ? `flash${flash}` : ''
return (
<div key={Math.random()} className={flashClass}>
{count}
</div>
)
}
I guess it forces React's diff algorithm to believe it needs to create a new DOM element each time because the key changes. I'm not sure how reliable that is though!
Update jsfiddle: https://jsfiddle.net/wd5bm20j/
I guess it forces React's diff algorithm to believe it needs to create a new DOM element each time because the key changes. I'm not sure how reliable that is though!
Yes, but in this case you might as well not use React at all 馃槈 . This is very inefficient and bad for performance.
@gaearon Do you have a suggestion for better performance? I have the same problem. Imagine an email application that needs to temporarily highlight the number of unread emails as they come in. You could also see something like this in an application with notifications as they increment (some small animation to draw attention).
Other than using a plugin/library to achieve this, how do you recommend writing this from scratch? Thanks!
The issue here is specific to any css transition and there are plenty of work around. I'd suggest checking out how libraries handle it (zepto is good reference lib). Basically tho you don't need changes to happen on separate ticks but you need to reflow the browser between changes
@jquense Thanks Jason, I'm looking for the specific/suggested react way to do this which I think would be helpful for those on the thread (or new visitors). I've searched for this quite a bit and haven't seen many good solutions, they all just key off of other libraries. React is opinionated about this behavior since it impacts performance, I would love to see examples of how developers should approach this properly in their react applications.
This is a tricky use case since the component is mounted once but the same css animation needs to be re-run on every increment/decrement. Since the class is the same, react does not register the change. A solid example here would be awesome.
This might not be an optimal solution but I solved it by having duplicate elements (divs) where I assign the css class (flash) alternately. E.g.
somethingHappened() {
this.setState({ toggle: !this.state.toggle });
}
render() {
return (
<div className={this.state.toggle === true ? myAnimationClass : null}>
<div className={this.state.toggle === false? myAnimationClass : null}>
.....
</div>
</div>
)
}
the only thing that affects performance here is intentionally destroying and remounting elements via a key
change.
My point above is that the correct solution is the same regardless of whether its React, Angular or Backbone, you need to deal with the idiosyncrasies of css animation from js.
here is the original example above working: https://codesandbox.io/s/2496rpxlkj
For my case I have a dropdown and when the element in the dropdown changes, the animation has to pop up in a dom element.
I have a boolean state variable that basically decides which class to set. If the variable is true I set the class, otherwise it's empty.
this.state = {
animationClass: true
}
In the onClick event of the dropdown:
setTimeout(() => {
this.setState({selectValue :val , animationClass: true});
},100);
and in the render method:
var flash = this.state.animationClass ? "bounce" : "";
return <div className = {flash}></div>
I've found a solution using two dirty things, one is calling void element.offsetWidth
to trigger a reflow from css tricks and other one is using findDOMNode
https://codepen.io/saitonakamura/pen/bLVLoN?editors=0110
I had a similar problem, my use case was a stock quote, and I needed to animate a cell to flash the background red or green depending on the direction of the change in the quote price. My solution is all css animation, resulting in only one change to the DOM (or to the component state) to reflect the updated price and apply the right classname for the background animation.
https://codepen.io/jmerecki/pen/VQaXoZ
I did not code this as a React component in the example because there isn't anything React specific. But hopefully this helps other people with a similar need to temporarily change the look of something on the screen and allow it animate back to its original state.
@jasonmerecki there is a problem with your code. Because if we want to show the animation again, for example in case of two consecutive raises, it won't work
aha! Thanks @saitonakamura. I actually need to fix that for my own needs too.
I'm not happy with my solution but at least it's working. Basically I duplicate all of the css and then flip out like "flashRed1" and "flashRed2". Since they are different classes, then the browser will apply the new class and the new animation.
https://gist.github.com/jasonmerecki/984b5d132077e7a370567c130c1922d9
https://codepen.io/jmerecki/pen/rJMpGK/
Here is a rough fiddle https://codesandbox.io/s/yj0o0yw3k1
With multiple items: https://codesandbox.io/s/jlp989m9q5
err: i mean codesandbox
I added the function below to index.html
```
$.fn.extend({
animateCss: function (animationName, callback) {
var animationEnd = (function (el) {
var animations = {
animation: 'animationend',
OAnimation: 'oAnimationEnd',
MozAnimation: 'mozAnimationEnd',
WebkitAnimation: 'webkitAnimationEnd',
};
for (var t in animations) {
if (el.style[t] !== undefined) {
return animations[t];
}
}
})(document.createElement('div'));
this.addClass('animated ' + animationName).one(animationEnd, function () {
$(this).removeClass('animated ' + animationName);
if (typeof callback === 'function') callback();
});
return this;
},
});
function AnimateError() {
$('#LoginBox').animateCss('shake');
}
Here is the react component
/* All the other html elements */I'm pretty sure this is not the correct way to do this. :)
With react hooks I encountered this problem. I attempt to restart an animation after each update. In useEffect I add the CSS animation. In the return portion of useEffect, I remove it so that it can be re-added the next update. But this doesn't retrigger the animation.
So then I tried alternating between two different CSS classes that do the same thing like what @jasonmerecki suggested. But it still didn't work! Finally I alternated between the typical animation class and a reverse of that animation played in reverse. This made it work!
Below is the codesandbox demonstrating this.
Most helpful comment
I found the following workaround:
I guess it forces React's diff algorithm to believe it needs to create a new DOM element each time because the key changes. I'm not sure how reliable that is though!
Update jsfiddle: https://jsfiddle.net/wd5bm20j/