Reputation: 810
I am making a simple notification service in React, and have encountered a very strange issue which I cannot find any mention of or solution to.
My component has an array of notification messages. Each "notification" has both a "onClick" and "onAnimationEnd" binding that call a function which will remove them from the array of notifications. The basic idea is that the notification will fade away (using a CSS animation) and then be removed, or allow the user to manually click on the notification to remove it.
The interesting bug is as follows. If you add two notifications, the first one will trigger its onAnimationEnd and remove itself. The remaining notification will suddenly jump to the end of its css animation and never trigger its own onAnimationEnd.
Even more curiously if you add four notifications, exactly two of them will have the above bug, while the other two function properly.
It is also worth mentioning that if you create two notifications, and click on one two manually remove it, the remaining notification will act normally and trigger its own onAnimationEnd functionality.
Thus I am forced to conclude that some combination of looping through an array and the onAnimationEnd trigger is bugged somehow in react, unless someone can point out a solution to this problem.
React Code
class Test extends React.Component {
constructor (props) {
super(props)
this.state = {
notifications: []
}
}
add = () => {
this.setState({notifications: [...this.state.notifications, 'Test']})
}
remove = (notification) => {
let notificationArray = this.state.notifications
notificationArray.splice(notificationArray.indexOf(notification), 1)
this.setState({notifications: notificationArray})
}
render() {
return (
<div>
<button onClick={this.add}>Add Notification</button>
<div className="notification-container">
{this.state.notifications.map(
(notification,i) => {
return (
<div onAnimationEnd={()=>this.remove(notification)}
onClick={() => this.remove(notification)}
className="notification"
key={i}>
{notification}
</div>
)
}
)}
</div>
</div>
);
}
}
ReactDOM.render(
<Test />,
document.getElementById('root')
);
CSS
.notification-container {
position: fixed;
bottom: 20px;
right: 20px;
width: 200px;
}
.notification {
border: 1px solid;
box-shadow: 0 10px 20px rgba(0,0,0,0.2), 0 6px 6px rgba(0,0,0,0.25);
color: white;
background: rgba(0,0,0,0.75);
cursor: pointer;
padding: 10px;
margin-top: 10px;
user-select: none;
animation: fade 7s linear forwards;
}
@keyframes fade {
0% {
transform: translate(100%,0);
}
2% {
transform: translate(-20px, 0);
}
5% {
transform: translate(0,0);
}
20% {
opacity: 1;
}
100% {
opacity: 0.25;
}
}
Working Codepen link https://codepen.io/msorrentino/pen/xeVrwz
Upvotes: 7
Views: 19159
Reputation: 1127
You're using an array index as your component key:
{this.state.notifications.map(
(notification,i) => {
return (
<div onAnimationEnd={()=>this.remove(notification)}
onClick={() => this.remove(notification)}
className="notification"
key={i}>
{notification}
</div>
)
}
)}
When you do this, React can't properly detect when your component is removed. For example, by removing the item at index 0, and moving the item at index 1 into its place, React will think the item with key 0 has merely been modified rather than removed. This can have various side-effects, such as what you're seeing.
Try using a unique identifier if you have one, otherwise use some kind of incrementing key.
To test this real fast in your codepen, change these bits (I don't actually recommend using your message as your key, of course):
add = () => {
this.setState({notifications: [...this.state.notifications, Math.random().toString()]})
}
...
key={notification}
Array indexes should only be used as component keys as a last resort.
Upvotes: 8