React OnAnimationEnd Issue - javascript

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

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.

Related

Sticky header animating when appearing but not when disappearing

edit: here is my sandbox https://codesandbox.io/s/nostalgic-morning-3f09m?file=/src/App.tsx
So, I have a sticky header made on React/Gatsby which should appear once the screen is scrollY >= 420. Once it hits 420px, it shows a nice animation sliding the header down. When I scroll the screen back up, however, the sticky header just "disappears" in a very cold way. The idea is that it would also "slide" up and disappear in a reverse way as it appeared. An example of what I want to achieve -> https://www.pretto.fr/
I want exactly this, for the header to slide when it comes down but when I scroll back up, for it scroll up disappearing.
The difference is that in this website the sticky header and the "main" header are two different components it seems. On my website, they are just one, and I'm just using props for it to go from position: relative; to position: sticky;
My header:
function Header(props: HeaderProps): React.ReactElement {
const [sticky, setSticky] = useState(false)
useEffect(() => {
document.addEventListener('scroll', trackScroll)
return () => {
document.removeEventListener('scroll', trackScroll)
}
}, [])
const trackScroll = () => {
if (typeof window == 'undefined') {
return
} else {
setSticky(window.scrollY >= 420)
}
}
return (
<Container id="container" sticky={sticky} className={`${sticky ? 'sticky' : ''}`}>
...
And my styled-components styles...
const smoothScroll = keyframes`
0% { transform: translateY(-100%); }
100% { transform: translateY(0px); }
`
const Container = styled.div<{ sticky?: boolean }>`
display: flex;
justify-content: space-between;
margin: auto;
padding: 0 6rem;
width: 100%;
position: ${props => (props.sticky ? 'sticky' : 'relative')};
top: 0px;
height: 97px;
align-items: center;
z-index: 3;
background: ${props => (props.sticky ? 'white' : 'inherit')};
&.sticky {
animation: ${smoothScroll} 500ms;
}
`
So the nice "sliding down" animation works once I scroll down to 420px. But as soon as I scroll back up it just disappears instead of "sliding up". Any ideas on how to achieve this?
The animation will fire only when 'sticky' class is added to the element, not removed. One of the possible solutions is to track scrolling up event and add another class with 'sliding up' animation that contains reversed styles - general concept

How to disable click events when execution is going on in React?

I have a screen
where I want to disable all the events when execution is going on.
When I click on the Execute button, an API is called which probably takes 4-5 minutes to respond. During that time, I don't want the user to click on the calendar cells or Month navigation arrows.
In short, I want to disable all the events on the center screen but not the left menu.
Is it possible to do that?
Yes sure, you can add a class with css pointer-events rule. Set it on the whole table and it will disable all events. Just add it when request starts and remove it when it ends. You can achieve that, by having a boolean variable isLoading in your state, or redux store and based on that add or remove the no-click class.
.no-click {
pointer-events: none;
}
You can use classic loading overlay box over your content when some flag (i.e. loading) is true.
Other way to do it is to use pointer-event: none in CSS. Use same flag to set class to your content block.
Here is a working example in codesanbox:
https://codesandbox.io/s/determined-dirac-fj0lv?file=/src/App.js
Here is code:
export default function App() {
const [loadingState, setLoadingState] = useState(false);
const [pointerEvent, setPointerEvent] = useState(false);
return (
<div className="App">
<div className="sidebar">Sidebar</div>
<div
className={classnames("content", {
"content-pointer-event-none": pointerEvent
})}
>
<button onClick={() => setLoadingState(true)}>
Show loading overlay
</button>
<button onClick={() => setPointerEvent(true)}>
Set pointer event in css
</button>
{loadingState && <div className="loading-overlay"></div>}
</div>
</div>
);
}
.App {
font-family: sans-serif;
text-align: center;
display: flex;
position: absolute;
width: 100%;
height: 100%;
}
.content {
flex: 1;
position: relative;
}
.loading-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
.content-pointer-event-none {
pointer-events: none;
}

React CSS Transition Inconsistency Through Updates

The below snippet has four boxes. The purpose is that these boxes order will be shuffled and a transition animation occurs as they go to a new location. Each box's key corresponds with a color value from the source array in useState. Each update via the shuffle button, the source array's values are shuffled. Then I map through the array in the return function. I set 2 classNames for each box. One classname corresponds with the index and is for positioning. The other classname corresponds with the source array value and is always in unison with the key for that box.
My issue is that react seems to randomly be deciding what keys to pay attention to and reconcile, and what keys to disregard and just remount those elements. You can see here, some elements properly transition while others just jump to their target location. I'm at a loss as to why this is occuring. Can someone help?
EDIT: I don't believe this is a reconcile issue with respect to unwanted remounting. React is properly respecting the keys and not remounting any. So the issue is with how React handles CSS transition classes added during updates. Some transitions work and others don't. It may just be a limitation of the engine, but if anyone has any further incite please share.
const {useState} = React;
function App() {
const [state, setState] = useState(['Red', 'Green', 'Blue', 'Black'])
function handleShuffle() {
const newState = _.shuffle(state)
setState(newState)
}
return (
<div className="App">
{state.map((sourceValue, index) => {
return (
<div className={
'box positionAt' + index + ' sourceValue' + sourceValue
}
key={sourceValue} ></div>
)
})}
<button id="shuffle" onClick={handleShuffle}> shuffle < /button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( <
App / > ,
rootElement
);
.App {
position: relative;
width: 200px;
height: 200px;
background-color: gray;
}
.box {
width: 25px;
height: 25px;
position: absolute;
transition: transform 1s;
}
.positionAt0 {
transform: translate(0px, 0px);
}
.positionAt1 {
transform: translate(175px, 0px);
}
.positionAt2 {
transform: translate(0px, 175px);
}
.positionAt3 {
transform: translate(175px, 175px);
}
.sourceValueGreen {
background-color: green;
}
.sourceValueBlue {
background-color: blue;
}
.sourceValueRed {
background-color: red;
}
.sourceValueBlack {
background-color: black;
}
#shuffle {
position: absolute;
top: 0px;
left: 75px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div id="root"></div>
Took me a while to figure it out, because setting the correct keys for the boxes seemed to be just the right thing.
I verified with the developer tools that the keys work, by inspecting a box and storing it in a variable (b = $0), shuffling, re-inspecting the box with the same key (color) and comparing it with the stored node ($0 === b, was true). So The DOM nodes for each key are stable.
But it's not sufficient for CSS transitions because the way browsers are changing the order of elements in the DOM.
You can see it here in a minimized example for efficiently reordering elements in the DOM (I assume that React does similar things internally when elements have to be reordered):
function reorder() {
const list = document.querySelector("ul");
list.appendChild(list.firstElementChild);
}
<ul>
<li>List-item #1</li>
<li>List-item #2</li>
</ul>
<button onclick="reorder()">reorder!</button>
Run the example and set a DOM breakpoint on the resulting <ul> DOM node for "Subtree modification", see screenshot.
If you click on "reorder!", the browser breaks first on the removal of a <li>. If you continue, and immediately after continuing (Firefox: <F8>) the browser breaks again with an insertion of a <li>.
(In my tests, the information Chrome gave about the breaks was a bit misleading, Firefox was better at that)
So the browsers implement reordering technically as "remove and insert", which breaks CSS transitions.
With that knowledge the code can easily be fixed by having fixed order of the boxes in the DOM (The order in DOM doesn't need to be changed, because the position is only set via classes):
(Note: *HTML and CSS unchanged, changes in JavaScript are marked with NEW or CHANGE *)
const {useState} = React;
// NEW: List of boxes for a stable order when rendering to the DOM:
const boxes = ['Red', 'Green', 'Blue', 'Black'];
function App() {
const [state, setState] = useState(boxes); // CHANGE: reuse boxes here
function handleShuffle() {
const newState = _.shuffle(state)
setState(newState)
}
return (
<div className="App">
{/* CHANGE: loop over boxes, not state and lookup position, which is used for the positionAt... class */
boxes.map((sourceValue, index) => {
const position = state.indexOf(sourceValue);
return (
<div className={
'box positionAt' + position + ' sourceValue' + sourceValue
}
key={sourceValue} ></div>
)
})}
<button id="shuffle" onClick={handleShuffle}> shuffle < /button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( <
App / > ,
rootElement
);
.App {
position: relative;
width: 200px;
height: 200px;
background-color: gray;
}
.box {
width: 25px;
height: 25px;
position: absolute;
transition: transform 1s;
}
.positionAt0 {
transform: translate(0px, 0px);
}
.positionAt1 {
transform: translate(175px, 0px);
}
.positionAt2 {
transform: translate(0px, 175px);
}
.positionAt3 {
transform: translate(175px, 175px);
}
.sourceValueGreen {
background-color: green;
}
.sourceValueBlue {
background-color: blue;
}
.sourceValueRed {
background-color: red;
}
.sourceValueBlack {
background-color: black;
}
#shuffle {
position: absolute;
top: 0px;
left: 75px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div id="root"></div>
Note: it now works even if no key is set.

Make so that div changes color for a brief time when clicked

I've got a setup where I'm using divs as buttons, and when they're clicked they add to ingredients to my burger.
JS:
<div id="ingredientBox">
<Ingredient
ref="ingredient"
v-for="item in currentIngredients"
v-on:increment="addToBurger(item)"
:item="item"
:lang="lang"
:ui-labels="uiLabels"
:key="item.ingredient_id">
</Ingredient>
</div>
With CSS:
.ingredient {
border: 1px solid #f5f5f28a;
padding: 0.8em;
width: 23vh;
height: 19vh;
background-color: green;
border-radius: 25px;
margin: 10px;
text-align: center;
}
I now want the div to react visually when clicked (maybe change color for like 0.2 seconds or something. I've looked around and only find info on how to change color permanently, is there a simple way of changing the color for just a brief moment?
You can use CSS keyframe animation to pull this off:
#keyframes animate-burger-button {
0% {
background: red;
}
50% {
background: yellow;
}
100% {
background: green;
}
}
#ingredientBox:active {
animation: animate-burger-button 0.8s forwards;
}
I would also add another note to try and use a button instead of a div, make accessibility a lot easier.
You could do something like
#ingredientBox:active {
color: red;
}
You could use setTimeout to add a class to the button and then remove it.
code:
buttonTrigger() {
element.classList.add('somesyle'); // add colour changing class to element
setTimeout(() => {
element.classList.remove('somestyle'); //remove the class after 0.2 seconds
}, 200)
}
EDIT
I was going to also suggest using CSS keyframes but #AlexanderKaran already suggested it. That is a good option too.

React: Stop a styled-components animation from running when the component is first mounted

I have a styled-component that receives props to determine what animation to use. This is controlling an arrow icon that when active rotates 'open' and when inactive remains 'closed'.
Here is what the styled-component and two keyframes animations look like:
const rotate_down = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(90deg);
}
`;
const rotate_up = keyframes`
from {
transform: rotate(90deg);
}
to {
transform: rotate(0deg);
}
`;
const OpenUserSettings = styled.div`
cursor: pointer;
width: 20px;
height: 20px;
transition: 0.3s;
animation: ${props => (props.rotate ? rotate_down : rotate_up)} 0.5s ease
forwards;
margin-top: 2px;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
& > img {
width: 5px;
}
`
The passed in rotate prop is a boolean value that is toggled via an onClick handler in the React component:
<OpenUserSettings
rotate={arrowDown}
onClick={() => {
this.setState(prevState => ({
arrowDown: !prevState.arrowDown
}));
}}
>
<img src={OpenArrow} alt="menu drop down arrow" />
</OpenUserSettings>
This works, and when the arrow is clicked, rotate prop is passed into OpenUserSettings and it successfully toggles between the rotate_up and rotate_down keyframes.
Now my problem is that when the React component first mounts, the arrowDown default is set to false meaning that the rotate prop is going to be false. This causes the styled-component to set the animation to rotate_up the first time mounting. I figured this would be hard to visualize so check out this to see what I am describing:
You can see when the page is refreshed the rotate_up animation is firing very quickly. I need the arrow to stay closed, but I do not need the rotate_up animation to fire when first loaded to close it. Is this a situation for something like react-transition-group where I can control the initial enter or is it something that can be handled with logic?
My first contribution to stack overflow, how exciting!
To assure the animation does not render on mounting, initialise your state with something else as true/false, I remember setting my state to null initially.
In your styled component you can pass null / true / false as a prop and define a function outside your component that performs checks on the null / true / false. (I used TypeScript, so don't copy the types)
function showAnimationOrNot(status: boolean | null) {
if (status === true) {
return `animation: nameAnimationIn 1s ease forwards;`
} else if (status === false) {
return `animation: nameAnimationOut 1s ease-out forwards;`
} else if (status === null) {
return ""
}
}
In your styled component simply add a line:
${(props: PropsDisplay) => show(props.show)};
This is how I did it, I am sure there are more elegant ways, but it did the trick of preforming the animation on mounting.
In return if somebody knows how to get styled component syntax highlighting inside the return of functions like the one I used here (from the example above):
return `animation: comeout 1s ease-out forwards;`
Do let me know! looking for that! Thank you and good luck!
Keep on animating! :)
I think you'll have an easier time with a CSS transform/transition here, rather than an animation. Here's the code:
const rotateDeg = props.rotate ? '90deg' : '0deg';
const OpenUserSettings = styled.div`
cursor: pointer;
width: 20px;
height: 20px;
transition: all 0.3s ease;
transform: rotate(${rotateDeg});
margin-top: 2px;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
& > img {
width: 5px;
}
`
I didn't quite understand what position you wanted the arrow in, so you might need to flop 0deg and 90deg and I think it might actually be -90deg, but hopefully this helps.
Edit
Your question was actually how to stop an animation. You can pause a css animation with animation-play-state. In this case you could have had the component mount with animation-play-state: paused; and then added an additional prop to change that to running on first click, but that seems unnecessarily complicated IMO.
More on animation-play-state.

Categories