React way

  • react-motion
  • react-transition-group

Another way

  • GSAP
  • AnimJS
  • jQuery

"Размер имеет значение" (с) Woman

react-motion: ~20Kb
react-transition-group: ~6Kb
AnimeJS: ~12Kb
GSAP
  • TweenLite (~9Kb)
  • TimelineLite (~4Kb)
  • CSSPlugin (~15Kb)
  • EasePack (~2Kb)
В сумме: ~30Kb
или сразу TweenMax ~36Kb

Example

Note: по возможности реализовать фильтр, который позволяет переключать на четные или нечетные карточки.
Данные
			const arrCards = [
				{
					key: 1,
				}, {
					key: 2,
				}, {
					...
				}
			]
		
Разметка
			<div class="card-wrap">
				<div class="card">1</div>
				<div class="card">2</div>
				<div class="card">...</div>
			</div>
		

React way

react-motion

				getDefaultStyles() { // нужно создать метод, который будет определять
			                            исходное состояние для каждого элемента (массив объектов)
					return this.props.children.map(()=> {
						return {
							opacity: 0,
							translateY: 50,
						};
					});
				}
				getStyles(prevInterpolatedStyles) { // также метод, который будет описывать саму анимацию, использую
			                                           предыдущие состояния (они передаются аргументом)
					return prevInterpolatedStyles.map((_, i) => {
						if (i === 0) {
							return {
								opacity: spring(1, [30, 11]), // функция spring, грубо говоря, задает тип анимации, на
			                                                     подобии ease, ease-in-out и т.д.
								translateY: spring(0, [30, 11]),
							};
						} else {
							return {
								opacity: spring(prevInterpolatedStyles[i - 1].opacity),
								translateY: spring(prevInterpolatedStyles[i - 1].translateY),
							};
						}
					});
				}
		
			render() {
				return (
				    // Компонент <StaggeredMotion> как раз рассчитан на анимирование коллекции элементов
				    // В children'ы передается функция, которая будет вызваться всю анимацию
					<StaggeredMotion defaultStyles={this.getDefaultStyles()} styles={this.getStyles}>
						{(interpolatingStyles) =>
							<div className="card-wrap">
								{
									interpolatingStyles.map((item, i) => {
										return (
											<div
												key={this.props.children[i].key}
												className="card"
												style={{
													// собственно значения, которое высчитываются в getStyles
													   (изначально состояние выставляется из getDefaultStyles)
													opacity: `${item.opacity}`,
													transform: `translateY(${item.translateY}px)`
												}}
												datatype={this.props.children[i].type}>
													{this.props.children[i].key}
												</div>
										);
									})
								}
							</div>
						}
					</StaggeredMotion>
			   );  
			 }
		

react-transition-group

Тоже самое, что и react-motion, но своими руками и оперируя классами

(setState + setTimeout + Recursion = )

			.card-appear,
			.card-enter {
			  opacity: 0;
			  transform: translateY(50px);
			}
			
			.card-appear-active,
			.card-enter-active {
			  opacity: 1;
			  transform: translateY(0);
			  transition: opacity 0.4s ease,
			  transform 0.4s ease;
			}
			
			.card-leave {
			  opacity: 1;
			}
			
			.card-leave-active {
			  opacity: 0;
			  transition: opacity 0.5s ease;
			}
		
Название состояний по умолчанию

Another way

В данном случае встает вопрос как получить DOM элемент, который нужно анимировать. У нас есть следующие варианты

Для нашей задачи нам достаточно будет ref.

Функция render для всех случаев

			render() {
				const arrCards = this.props.children;
			 
				return (
				<div className="card-wrap">
					{
						arrCards.map((item) => {
							return (
								<div
									key={item.key}
									className="card"
									// заносим в локальную коллекцию все эл-ты 
									ref={(elem) => { this.arrCards.push(elem); }}
								>
									{item.key}
								</div>
							);
						})
					}
				</div>
				);
  			}
		

AnimJS

			fadeIn() {
				let appearTime = 400;
				let nextElementDelay = 0.2
					
				let animeJS = anime.timeline();
					
				animeJS.add({
					targets: this.arrCards,
					duration: 1,
					opacity: 0,
					translateY: 50,
				});
					
				let index = 0;
				arrCards.forEach((elem) => {
					let delay = index++ * nextElementDelay * 1000;
					
					animeJS.add({
						targets: elem,
						duration: appearTime,
						opacity: 1,
						translateY: 0,
						easing: 'linear',
						offset: delay,
					});
				});
			}
		

jQuery

GSAP

			fadeIn() {
				let appearTime = 0.4;
				let nextElementDelay = 0.2;
				let timeline = new TimelineLite();
			 
				timeline
					.staggerFromTo(this.arrCards, appearTime, {
						display: 'none',
						opacity: 0,
						y: 50,
					}, {
						display: 'block',
						opacity: 1,
						y: 0,
					}, nextElementDelay);
			}
		

Conclusion

React way

Плюсы
  • за изменения в DOM продолжает отвечать React
Минусы
  • анимация производится путем изменения state
  • нужно самому делать сложные расчеты
  • нет гибкости

Another way

Плюсы
  • более читаемый код
  • создание сложных анимаций*
Минусы
  • не уживается с лайфцайклом React
  • в закрытую модифицирует DOM от React
  • подходит лишь в ситуациях, когда состояние эл-та ограничивается лишь видимостью или невидимостью

Панацеи не существует

Но, так или иначе, и React way, и Another way позволяют решать одни и те же задачи, если оба способа заставить работать в содружестве.

react-transition-group + GSAP

Почему react-transition-group?

Почему GSAP?

GSAP API (то, чего хватает для работы)

Релазиуем появление уведомления

Описание задачи

Плашка должна появляться сверху вниз и исчезать в обратном порядке, то есть снизу вверх.

			import TransitionGroup from 'TransitionGroup';
			import TimelimeLite from 'gsap/TimelimeLite';
			import 'gsap/CSSPlugin';
			import 'gsap/EasePack';
				
			function Notice(props) {
				const { message, ...props } = props;
				return (
					<div className="notice" {...props}>
						{message}
					</div>
				);
			}
			export default function (props) {
				const { animate, duration, message } = props;
				
				if (animate) {
					return (
						<TransitionGroup>
							{ message ? <NoticeContainer message={message} duration={duration} /> : null }
						</TransitionGroup>
					);
				} else {
					return message ? Notice({ message }) : null;
				}
			};
		
			class NoticeContainer extends Component {
				defaultProps = {
					duration: 0.7, // длительность анимации
				};
				elem = null; // DOM эл-т
				rect = {}; // объект с координатами эл-та
				height = 0; // высота эл-та
				timeline = new TimelineLite(); // GSAP
				
				componentDidMount() {
					this.show();
				}
				
			  // callback дает знать, что компонент готов к удалению
				componentWillLeave(callback) {
					this.hide(callback);
				}
				
				componentWillUnmount() {
					 // очищаем объект таймлайна, чтобы он не хранил ссылку на DOM  эл-т
					this.timeline = null;
				}
		
				show() {
					this.rect = this.elem.getBoundingClientRect();
					this.height = this.rect.height + this.rect.top;
					this.timeline
							.fromTo(this.elem, this.props.duration, {
								y: `-${this.height}px`,
							}, {
								y: 0,
								ease: Bounce.easeOut,
							});
				}
				hide(onComplete) {
					this.timeline
							.to(this.elem, this.props.duration, {
								y: `-${this.height}px`,
								onComplete: onComplete,
							});
				}
				getNode(node) {
					this.elem = node;
				}
				render() {
					return Notice({
							ref: this.getNode.bind(this),
							message: this.props.message,
					});
				}
			}