Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save LucaColonnello/f72fac56a183b0a0abdf to your computer and use it in GitHub Desktop.
Save LucaColonnello/f72fac56a183b0a0abdf to your computer and use it in GitHub Desktop.
Custom CSSTransitionGroup for React

Custom CSSTransitionGroup for React

The CSSTransitionGroup in the React Addons works fine for simple things, but since React components don’t have much knowledge of their children, more complex nested components can be re-rendering while the transitions are happening. This can lead to jerky animation.

This example implements a custom AnimationItem component that sits inside the TransitionGroup — note the lack of the CSS prefix there — which has an API for handling the various transition events. I’ve reimplemented the behaviour from the CSSTransitionsGroup in the AnimationItem component but using requestAnimationFrame to wait a render tick before triggering the transition, and Arrival to determine when the transition ends.

Again, it’s unnecessary in this example because the components are so simple, but it makes a massive difference on more complex components.

Forked from Max's Pen Custom CSSTransitionGroup for React.

Forked from Max's Pen Custom CSSTransitionGroup for React.

A Pen by Luca Colonnello on CodePen.

License.

<div id="list"></div>
var ReactTransitionGroup = React.addons.TransitionGroup;
var AnimationItem = React.createClass({
componentWillEnter: function(done) {
this.el = this.getDOMNode();
this.$el = $(this.el);
// Before state applied immediately
this.$el.addClass(this.props.prefix+"-enter-before");
// Ensure parent class changes propagate in time
requestAnimationFrame(function() {
this.$el
.removeClass(this.props.prefix+"-enter-before")
.addClass(this.props.prefix+"-enter");
requestAnimationFrame(function() {
getComputedStyle(this.el);
this.$el.addClass(this.props.prefix+"-enter-active");
Arrival.complete(this.$el, done);
}.bind(this));
}.bind(this));
},
componentDidEnter: function() {
this.$el
.removeClass(this.props.prefix+"-enter")
.removeClass(this.props.prefix+"-enter-active");
},
componentWillLeave: function(done) {
this.el = this.getDOMNode();
this.$el = $(this.el);
// Before state applied immediately
this.$el.addClass(this.props.prefix+"-leave-before");
requestAnimationFrame(function() {
this.$el
.removeClass(this.props.prefix+"-leave-before")
.addClass(this.props.prefix+"-leave");
requestAnimationFrame(function() {
this.$el.addClass(this.props.prefix+"-leave-active");
Arrival.complete(this.$el, done);
}.bind(this));
}.bind(this));
},
componentDidLeave: function() {
this.$el
.removeClass(this.props.prefix+"-leave")
.removeClass(this.props.prefix+"-leave-active");
},
render: function() {
return (
React.DOM.div(
{className: "animation-item", key: this.props.key},
this.props.children
)
)
}
});
var TodoList = React.createClass({
getInitialState: function() {
return {items: ['Hello', 'World', 'Click', 'Me']};
},
handleAdd: function() {
var newItems =
this.state.items.concat([prompt('Enter some text')]);
this.setState({items: newItems});
},
handleRemove: function(i) {
var newItems = this.state.items;
newItems.splice(i, 1);
this.setState({items: newItems});
},
render: function() {
var items = this.state.items.map(function(item, i) {
var element = React.DOM.div(
{ key: item, className: "item", onClick: this.handleRemove.bind(this, i) },
item
);
return (
AnimationItem({key: "example-"+item, prefix: "example"}, element)
);
}.bind(this));
return (
React.DOM.div(
null,
React.DOM.div(
null,
React.DOM.button({onClick: this.handleAdd}, "Add item")
),
ReactTransitionGroup(
null,
items
)
)
);
}
});
React.renderComponent(
TodoList(),
document.getElementById('list')
);
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller
// fixes from Paul Irish and Tino Zijdel
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/0.10.0/react-with-addons.js"></script>
<script src="https://cdn.rawgit.com/icelab/arrival/0.0.1/arrival.js"></script>
body {
font-family: Helvetica;
padding: 50px 100px;
}
button {
margin-bottom: 20px;
}
.item {
background-color: #efefef;
cursor: pointer;
display: block;
margin-bottom: 1px;
padding: 8px 12px;
width: 100px;
&:hover {
background-color: #ddd;
}
.example-enter & {
opacity: 0;
transform: translate(-250px,0);
transform: translate3d(-250px,0,0);
}
.example-enter.example-enter-active & {
opacity: 1;
transform: translate(0,0);
transform: translate3d(0,0,0);
transition-property: transform, opacity;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.175, 0.665, 0.320, 1), linear;
}
.example-leave & {
opacity: 1;
transform: translate(0,0,0);
transform: translate3d(0,0,0);
transition-property: transform, opacity;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.175, 0.665, 0.320, 1), linear;
}
.example-leave.example-leave-active & {
opacity: 0;
transform: translate(250px,0);
transform: translate3d(250px,0,0);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment