TransitionGroup and CssTransition: Exit transition not applied - javascript

I migrated from the old CSSTransitionGroup to the new react-transition-group CSSTransition and TransitionGroup.
I'm creating (hacking) an overlay loader and I'm trying to apply animation to the overlay when it appears and disappears.
Specifically when I pass an active=true props to the LoadingOverlayWrapper then a new CSSTransition is added to the TransitionGroup (the Fadecomponent) that wraps the overlay to show.
When active=false then the CSSTransition is removed from within the TransitionGroup (the direct child of TransitionGroupis null).
This is the relevant part of the code:
import React, {Children} from 'react'
import PropTypes from 'prop-types'
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import LoadingOverlay from "./LoadingOverlay";
import styles from './Overlay.sass';
const FirstChild = props => Children.toArray(props.children)[0] || null;
const Fade = (props) => (
<CSSTransition
{...props}
timeout={500}
classNames={{
appear: styles.appear,
appearActive: styles.appearActive,
enter: styles.enter,
enterActive: styles.enterActive,
exit: styles.exit,
exitActive: styles.exitActive
}}
>
<FirstChild {...props} />
</CSSTransition>
);
class LoadingOverlayWrapper extends React.Component {
render() {
const {active} = this.props;
return (
<div>
<TransitionGroup>
{
active ?
(
<Fade key='transition_effect'>
<LoadingOverlay key='the_dimmer' {...this.props} />
</Fade>
)
:
null
}
</TransitionGroup>
{this.props.children}
</div>
)
}
}
And this is the relevant sass file (imported as css module):
.enter, .appear
opacity: 0.01
.appearActive, .enterActive
opacity: 1
transition: opacity .5s ease-in
.exit, .leave
opacity: 0.01
.exitActive, .leaveActive
opacity: 0
transition: opacity .5s ease-in
The enter (or appear, not sure here) transition works.
The problem is that when I remove the Fade component, being replace by null then the exit transition is not applied (or not visible) but I get no error, everything else works as intended.
I'm not sure how to debug or proceed here given I have little experience with React TransitionGroup.

I've been struggling with the same issue - the solution that worked for me is using the childFactory prop on the <TransitionGroup> like so:
<TransitionGroup
childFactory={child => React.cloneElement(child)}
>
{
active ?
(
<Fade key='transition_effect'>
<LoadingOverlay key='the_dimmer' {...this.props} />
</Fade>
)
:
null
}
</TransitionGroup>

import { CSSTransition } from 'react-transition-group';
<CSSTransition
in={toShow} // boolean value passed via state/props to either mount or unmount this component
timeout={300}
classNames='my-element' // IMP!
unmountOnExit
>
<ComponentToBeAnimated />
</CSSTransition>
NOTE: Make sure to apply below styles using the class property in CSS:
.my-element-enter {
opacity: 0;
transform: scale(0.9);
}
.my-element-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.my-element-exit {
opacity: 1;
}
.my-element-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}

Related

How to add animations on every re-render of component

I have a simple component of my trivia-app on React. When I answer a question - my component re-renders the new question and I want to have an animation for the question block. But, I have only the one-time animation at the first render of this component. How to make animation on every re-render of the component?
Here is my sandbox example - https://codesandbox.io/s/youthful-ardinghelli-hn2zx?file=/src/App.js
And here is my code:
import { useState } from "react";
import "./styles.css";
const questions = ["1", "2", "3", "4", "5"];
const answers = ["asgas", "asgasg", "ashash", "hasdha"];
export default function App() {
const [currentQuestion, setCurrentQuestion] = useState(0);
return (
<>
<h2 className="question">{questions[currentQuestion]}</h2>
<div>
{answers.map((i) => (
<button
onClick={() => {
setCurrentQuestion((prevState) => prevState + 1);
}}
key={i}
>
{i}
</button>
))}
</div>
</>
);
}
.App {
font-family: sans-serif;
text-align: center;
}
.question {
background-color: blue;
animation: fromTop 0.5s linear;
}
#keyframes fromTop {
0% {
transform: translateY(-20%);
}
100% {
transform: translateY(0%);
}
}
Just add something data to <h2>-tag, then React can re-render this tag and animation started.
<h2 className="question" key={currentQuestion}>{questions[currentQuestion]}</h2>
You cannot use a useEffect(), because this will not animate as the component hasn't been "drawn" yet.
useEffect is executed after the render but before the "drawing" of the components, you have to use the hook useLayoutEffect. This hook executes after the drawing of the component, so you will have a lot more information about the layout like position, height, or weight.

Conditional Rendering and ReactCSSTransitionGroup Animation

I've made a small app that renders different components based on a Redux state. I want to apply a "fade" animation when one of the component renders. However, for some reason, it doesn't work for me. Here's what I have so far:
content.js
class Content extends Component {
render() {
const transitionOptions = {
transitionName: "fade",
transitionEnterTimeout: 500,
transitionLeaveTimeout: 500
}
if (this.props.page === 'one') {
return (
<div>
<ReactCSSTransitionGroup {...transitionOptions}>
<Comp1/>
</ReactCSSTransitionGroup>
</div>
);
} else {
return (
<div>
<ReactCSSTransitionGroup {...transitionOptions}>
<Copm2/>
</ReactCSSTransitionGroup>
</div>
);
}
}
}
style.css
.fade-enter {
opacity: 0.01;
}
.fade-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.fade-leave {
opacity: 1;
}
.fade-leave.fade-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
I've seen ReactCSSTransitionGroup being used for items being added and removed to a list, but I haven't found one example of it being used for conditional rendering. Is it achievable? Maybe there's another addon that does this?
I've seen this same problem posted many times. In short: you need to conditionally render the children inside of <ReactCSSTransitionGroup>, not <ReactCSSTransitionGroup> itself. <ReactCSSTransitionGroup> needs to mount once and then stay, it's the children that get added and removed.
content.js
class Content extends Component {
render() {
const transitionOptions = {
transitionName: "fade",
transitionEnterTimeout: 500,
transitionLeaveTimeout: 500
}
let theChild = undefined;
if (this.props.page === 'one') {
theChild = <Comp1 key="comp1" />;
} else {
theChild = <Comp2 key="comp2" />;
}
return (
<div>
<ReactCSSTransitionGroup {...transitionOptions}>
{theChild}
</ReactCSSTransitionGroup>
</div>
);
}
}
Note that you should also add a unique key prop to each child inside of a <ReactCSSTransitionGroup>. That helps the component identify which children are unique in order to properly animate them in and out.
Here is a snippet from my code
render() {
return (
<CSSTransitionGroup
transitionName="slide"
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
>
{ id === targetID ? (
<div>
<SectionList id={id} />
</div>
) : '' }
</CSSTransitionGroup>
)
}

React animate with CSSTransitionGroup on toggle

I am trying to get a transition between components entering and leaving when the button is clicked to toggle between state
I have also tried putting <UserDetails /> and <UserEdit /> as seperate components but have the same result in that no animation is triggered.
https://www.webpackbin.com/bins/-KjIPcMeQF3iriHRqTBW
Hello.js
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'
import './styles.css'
export default class Hello extends React.Component {
constructor() {
super()
this.state = { showEdit: false }
this.handleEdit = this.handleEdit.bind(this)
}
handleEdit() { this.setState({ showEdit: !this.state.showEdit }) }
render() {
const UserDetails = () => (
<div className="componenta" key="1">UserDetails</div>
)
const UserEdit = () => (
<div className="componentb" key="2">UserEdit</div>
)
return(
<div >
<CSSTransitionGroup
transitionName="example"
transitionEnterTimeout={10}
transitionLeaveTimeout={600}>
{this.state.showEdit ? <UserDetails /> : <UserEdit />}
</CSSTransitionGroup>
<button onClick={this.handleEdit}>Toggle</button>
</div>
)
}
}
styles.css
.thing-enter {
opacity: 0.01;
transition: opacity 1s ease-in;
}
.thing-enter.thing-enter-active {
opacity: 1;
}
.thing-leave {
opacity: 1;
transition: opacity 1s ease-in;
}
.thing-leave.thing-leave-active {
opacity: 0.01;
}
You have your key attribute set to the wrong item - the <div> is not the direct descendant of CSSTransitionGroup (because your wrapped it into a component by defining it as a function), so it doesn't know which items were added or removed. You have to set your keys to UserDetails and UserEdit, so CSSTransitionGroup can properly determine changes in it's children.
Here is your render method that works:
render() {
const UserDetails = () => (
<div className="componenta">UserDetails</div>
)
const UserEdit = () => (
<div className="componentb">UserEdit</div>
)
return(
<div >
<CSSTransitionGroup
transitionName="example"
transitionEnterTimeout={10}
transitionLeaveTimeout={600}>
{this.state.showEdit ? <UserDetails key="1" /> : <UserEdit key="2" />}
</CSSTransitionGroup>
<button onClick={this.handleEdit}>Toggle</button>
</div>
)
}
An alternative would be to store UserDetails and UserEdit into variables and then render them as such. That will allow you to leave the key attributes where they are now, instead of moving them.
Here is a working example using display:none to hide the unmounting component
enter link description here

React smooth transition between different states in a component

I have a simple component like this:
var component = React.createClass({
render: function(){
if (this.props.isCollapsed){
return this.renderCollapsed();
}
return this.renderActive()
},
renderActive: function(){
return (
<div>
...
</div>
);
},
renderCollapsed: function(){
return (
<div>
...
</div>
);
},
});
Basically, when the property changes, the component will either show active state or collapse state.
What I am thinking is, when the property change happens, i.e. active->collapse, or the other way around, I want the old view "shrink" or "expand" smoothly to show the new view. For example, if it is active -> collapse, I want the active UI to shrink to the size of collapse UI, and show it smoothly.
I am not sure how to achieve this effect. Please share some ideas. Thanks!
Here is a minimal working example:
const collapsible = ({active, toggle}) =>
<div>
<button type="button" onClick={toggle}>Toggle</button>
<div className={'collapsible' + (active? ' active': '')}>
text
</div>
</div>
const component = React.createClass({
getInitialState() {
return {active: false}
},
toggle() {
this.setState({active: !this.state.active})
},
render() {
return collapsible({active: this.state.active, toggle: this.toggle})
}
})
ReactDOM.render(React.createElement(component), document.querySelector('#root'))
.collapsible {
height: 1.5rem;
transition: height 0.25s linear;
background: #333;
border-radius: 0.25rem
}
.collapsible.active {
height: 7rem
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div id="root"></div>
The view can be "shrink" or "expand" smoothly by CSS transition, which is triggered by changing CSS properties.
To control CSS properties with React, we can reflect state changes to property values or className in render().
In this example, .active class affects height value, and is controlled by state.active. The class is toggled by React in response to state changes, and triggers CSS transition.
For smoother transitions, See this article.
Rather than conditionally render two different end states of a
component, you could instead toggle the class on the same
component. You could have active and collapsed classes as follows:
For example:
.active{
-webkit-transition: -webkit-transform .5s linear; // transition of
// 0.5 of a second
height: 200px;
}
.collapsed{
height: 0px;
}
Check out this resource for examples
The standard way is to use CSSTransitionGroup from react-transition-group, which is quite easy. Wrap the component with the CSSTransitionGroup and set timeouts on enter and leave, like this:
<CSSTransitionGroup
transitionName="example"
transitionEnterTimeout={500}
transitionLeaveTimeout={300}>
{items}
</CSSTransitionGroup>
From the v1-stable docs:
"In this component, when a new item is added to CSSTransitionGroup it
will get the example-enter CSS class and the example-enter-active CSS
class added in the next tick."
Add styling for the CSS classes to get the correct animation.
There's also pretty good explanation in React docs, check it out.
There are third-party components for animation as well.
One more approach to this situation might be changing state after animation completes. The benefits of it is that you can apply not only transitions but whatever actions you want (js animations, smil, etc ..), main thing is not to forget to call an end callback;)
Here is working example CodePen
And here is the code example:
const runTransition = (node, {property = 'opacity', from, to, duration = 600, post = ''}, end) => {
const dif = to - from;
const start = Date.now();
const animate = ()=>{
const step = Date.now() - start;
if (step >= duration) {
node.style[property] = to + post;
return typeof end == 'function' && end();
}
const val =from + (dif * (step/duration));
node.style[property] = val + post;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
}
class Comp extends React.Component {
constructor(props) {
super(props);
this.state = {
isCollapsed: false
}
this.onclick = (e)=>{
this.hide(e.currentTarget,()=>{
this.setState({isCollapsed: !this.state.isCollapsed})
});
};
this.refF = (n)=>{
n && this.show(n);
};
}
render() {
if (this.state.isCollapsed){
return this.renderCollapsed();
}
return this.renderActive()
}
renderCollapsed() {
return (
<div
key='b'
style={{opacity: 0}}
ref={this.refF}
className={`b`}
onClick={this.onclick}>
<h2>I'm Collapsed</h2>
</div>
)
}
renderActive() {
return (
<div
key='a'
style={{opacity: 0}}
ref={this.refF}
className={`a`}
onClick={this.onclick}>
<h2>I'm Active</h2>
</div>
)
}
show(node, cb) {
runTransition(node, {from: 0, to: 1}, cb);
}
hide(node, cb) {
runTransition(node, {from: 1, to: 0}, cb);
}
}
ReactDOM.render(<Comp />, document.getElementById('content'));
And for sure, for this approach to work your only opportunity is to relay on state, instead of props of Component, which you can always set in componentWillReceiveProps method if you have to deal with them.
Updated
Codepen link updated with more clear example which is showing benefits of this approach. Transition changed to javascript animation, without relying on transitionend event.
As you want to render two different component for each of active and collapsed, wrap them in a div that controls the height with the help of CSS.
render: function(){
var cls = this.props.isCollapsed() ? 'collapsed' : 'expanded';
return(
<div className={cls + ' wrapper'}>
{
this.props.isCollapsed() ?
this.renderCollapsed() :
this.renderActive()
}
</div>
);
}
and in your CSS:
.wrapper{
transition: transform .5s linear;
}
.expanded{
height: 200px;
}
.collapsed{
height: 20px;
}
You can add a class that describes the active state ie. .active and toggle that class when switching states.
The css should look something like this:
.your-component-name{
// inactive css styling here
}
.your-component-name.active {
// active css styling here
}
Here you have a Toggle react component using the Velocity-React library, which is great for giving animations to transitions in React uis:
import React, { Component } from 'react';
import { VelocityTransitionGroup } from 'velocity-react';
export default class ToggleContainer extends Component {
constructor () {
super();
this.renderContent = this.renderContent.bind(this);
}
renderContent () {
if (this.props.show) {
return (
<div className="toggle-container-container">
{this.props.children}
</div>
);
}
return null
}
render () {
return (
<div>
<h2 className="toggle-container-title" onClick={this.props.toggle}>{this.props.title}</h2>
<VelocityTransitionGroup component="div" enter="slideDown" leave="slideUp">
{this.renderContent()}
</VelocityTransitionGroup>
</div>
);
}
};
ToggleContainer.propTypes = {
show: React.PropTypes.bool,
title: React.PropTypes.string.isRequired,
toggle: React.PropTypes.func.isRequired,
};
Hope it helps!

How can I animate a react.js component onclick and detect the end of the animation

I want to have a react component flip over when a user clicks on the DOM element. I see some documentation about their animation mixin but it looks to be set up for "enter" and "leave" events. What is the best way to do this in response to some user input and be notified when the animation starts and completes? Currently I have a list item and I want it to flip over an show a few buttons like delete, edit, save. Perhaps I missed something in the docs.
animation mixin
http://facebook.github.io/react/docs/animation.html
Upon clicks you can update the state, add a class and record the animationend event.
class ClickMe extends React.Component {
constructor(props) {
super(props)
this.state = { fade: false }
}
render() {
const fade = this.state.fade
return (
<button
ref='button'
onClick={() => this.setState({ fade: true })}
onAnimationEnd={() => this.setState({ fade: false })}
className={fade ? 'fade' : ''}>
Click me!
</button>
)
}
}
See the plnkr: https://next.plnkr.co/edit/gbt0W4SQhnZILlmQ?open=Hello.js&deferRun=1&preview
Edit: Updated to reflect current React, which supports animationend events.
React uses synthetic events, which includes animation events. Documention found here: https://reactjs.org/docs/events.html#animation-events. I updated the accepted answer below:
class ClickMe extends React.Component {
state = {fade: false};
render () {
const {fade} = this.state;
return (
<button
onClick={() => this.setState({fade: true})}
onAnimationEnd={() => this.setState({fade: false})}
className={fade ? 'fade' : ''}>
Click me!
</button>
)
}
}
Here is the answer using pure Reactjs events without any JQueries, other libs, registrations or sms :)
The key point is to provide animation keyframe name as a function parameter
CSS
.Modal {
position: fixed;
top: 30%;
left: 25%;
transition: all 0.3s ease-out;
}
.ModalOpen {
animation: openModal 0.4s ease-out forwards;
}
.ModalClosed {
animation: closeModal 0.4s ease-out forwards;
}
#keyframes openModal {
0% { transform: translateY(-100%); }
100% { transform: translateY(0); }
}
#keyframes closeModal {
0% { transform: translateY(0); }
100% { transform: translateY(-100%);}
}
JS
const modal = ({
isShown, isMounted,
initiateUnmountAction, unmountAction
}) => {
const cssClasses = [
"Modal",
props.isShown ? "ModalOpen" : "ModalClosed"
];
return (
<Fragment>
{isMounted && <div className={cssClasses.join(' ')}
onAnimationEnd={event =>
{event.animationName == "closeModal" && unmountAction}
}>
<h1>A Modal</h1>
<button className="Button" onClick={initiateUnmountAction}>
Dismiss
</button>
</div>}
</Fragment>
);
};
How to do it with Hooks and prevState so as to not update state inside your event handlers.
import { useState } from 'react'
export default function Animation() {
const [fade, setFade] = useState(false)
const triggerFade = () => {
setFade(prevState => {
return !prevState
})
}
return (
<div
onAnimationEnd={triggerFade}
className={fade ? 'fadedClass' : 'visibleClass'}
>
Watch me fade
</div>
<button onClick={triggerFade}>Click Me</button>
)
}
Most popular and easy to use package I came across:
https://www.npmjs.com/package/react-transition-group
Install:
npm install react-transition-group
Usage:
import { CSSTransition } from 'react-transition-group';
<CSSTransition
in={toShow} // boolean value passed via state/props to either mount or unmount this component
timeout={300}
classNames='my-element' // IMP!
unmountOnExit
>
<ComponentToBeAnimated />
</CSSTransition>
NOTE: Make sure to apply below styles using the class property in CSS:
.my-element-enter {
opacity: 0;
transform: scale(0.9);
}
.my-element-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.my-element-exit {
opacity: 1;
}
.my-element-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}
I've never used React, but if it uses CSS3 animations/transitions, you might be able to do something like this:
element.addEventListener( 'webkitTransitionEnd', function( event ) {
console.log( 'Complete');
}, false );
I successfully used this project in my react-hammer integration project there are some examples with hammer events and react animation.
It can also be done using CSS,
If you are using TailwindCSS, the answer would be, Add below Classes
hover:opacity-90 focus:opacity-100
else raw css
.class-name:hover{
opacity: 0.9;
}
.class-name:focus{
opacity: 1;
}
I was realise in that way, hope it's help someone.
CSS:
.mainStyle {
/*some-styles*/
height: 250px;
background-color: red;
}
.openAnimation {
animation: open-menu .4s linear forwards;
}
.closeAnimation {
animation: close-menu .4s linear forwards;
}
#keyframes open-menu {
0% {width: 0;} /*You can use animation what do You need*/
100% {width: 80%;}
}
#keyframes close-menu {
0% {width: 80%;}
100% {width: 0;}
}
JSX file
Import React, { useState } from React;
const AnimatedMenu = () => {
const [isOpen, setOpen] = useState(false);
const animationEndHandler = ({ animationName }) => {
if (animationName === 'open-menu') {
setOpen(true);
}
if (animationName === 'close-menu') {
setOpen(false);
}
};
return (
<>
<div
onAnimationEnd={(event) => animationEndHandler(event)}
className={isOpen ? 'mainStyles open' : 'mainStyles close'}
>
hide/show
</div>
<button onClick={() => setOpen(!isOpen)}>click me</button>
</>
);
};

Categories