使用React Hooks构建CSS的transition和animation
Web动效在Web应用中所起的作用就不说了。有人说,Web动效可以给Web起到锦上添花效果,也有人说,Web动效可以增加用户的粘性和吸引力。就目前来说,在Vue框架体系下,可以使用<transition>
组件来构建Web动画效果,其实在React体系下,也可以使用类似的方式来给Web元素添加动画效果。接下来,就和大家一起探讨,在React框架如何将CSS的trasition
和animation
运用到元素中,让Web元素动起来。如果你对这方面知识感兴趣的话,欢迎继续往下阅读。
React构建的组件运用动效时常碰到的问题
不知道你是否像我一样,在使用React构建的组件,在添加动效的时候,总是和自己期望的有所差异。比如说,构建一个弹窗,弹窗出来的时候有动效,弹窗移除的时候没有动效。就像下面这样的一个效果:
你可能已经发现了,弹窗窗现的时候,会有一个fadeIn
效果,但弹窗移除时并看不到fadeOut
效果。这主要是因为,弹窗移除时,整个弹窗的DOM直接就删除了,因此也没有机会让你能看到fadeOut
效果。那么在React构建的应用中,如何来改善这一点呢?这也是接下来要和大家一起探讨的话题。
使用react-transition-group
给React组件添加动效
在Vue框架中提供了<transition>
组件,在不同的生命周期中添加相应的类名,并在相应的类名中设置样式,从而给Vue组件添加动效:
在React中,我们可以使用一个类似Vue的<transition>
组件,即 react-transition-group
可以给React组件添加动画效果。
react-transition-group
提供了四个组件,<Transition>
、<CSSTransition>
、<SwitchTransition>
和<TransitionGroup>
。比如下面这个示例,就是采用<CSSTransition>
组件构建的一个带有动效的组件:
// src/index.js
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { Container, Button, Alert } from 'react-bootstrap';
import { CSSTransition } from 'react-transition-group';
import './styles.css';
function ModalComponent() {
const [showButton, setShowButton] = useState(true);
const [showMessage, setShowMessage] = useState(false);
return (
<Container style={{ paddingTop: '2rem' }}>
{showButton && (
<Button
onClick={() => setShowMessage(true)}
size="lg"
>
Show Message
</Button>
)}
<CSSTransition
in={showMessage}
timeout={300}
classNames="alert"
unmountOnExit
onEnter={() => setShowButton(false)}
onExited={() => setShowButton(true)}
>
<Alert
variant="primary"
dismissible
onClose={() => setShowMessage(false)}
>
<Alert.Heading>
Animated alert message
</Alert.Heading>
<p>
This alert message is being transitioned in and
out of the DOM.
</p>
<Button onClick={() => setShowMessage(false)}>
Close
</Button>
</Alert>
</CSSTransition>
</Container>
);
}
ReactDOM.render(
<ModalComponent />,
document.getElementById('root')
);
// src/style.css
.alert-enter {
opacity: 0;
transform: scale(0.9);
}
.alert-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.alert-exit {
opacity: 1;
}
.alert-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}
效果如下:
有关于react-transition-group
更详细的介绍,还可以阅读:
构建transition
的React钩子函数
在组件生命周期中,有关于transition
的主要阶段有:mounted
、entering
、entered
、exiting
、exited
和unmounted
。
当一个组件被加载时,意味着它以DOM元素的形式出现,而不管它的样式如何。当它被卸载时,则从DOM中删除。传统上,在一个元素加载之后,动画在entered
和entering
阶段并没有问题,但是在卸载元素时,动画就比较棘手,因为React会在transition
之前删除元素。因此,我们需要一个时间来过渡,在删除元素之前隐藏它。
接下来,我们从构建一个React的钩子函数useTransitionState
开始,它从entering » entered
和exiting » exited
过程中提供延迟时间duration
来改变状态。在这里会使用useEffect
钩子函数,在DOM更新后执行一个计时器。另外,useTransitionState
作为一个清理函数,我们将确保计时器被清除,以避免内存泄漏。
const STATE = {
ENTERING: "entering",
ENTERED: "entered",
EXITING: "exiting",
EXITED: "exited"
};
const useTransitionState = (duration = 1000) => {
const [state, setState] = useState();
useEffect(() => {
let timerId;
if (state === STATE.ENTERING) {
timerId = setTimeout(() => setState(STATE.ENTERED), duration);
} else if (state === STATE.EXITING) {
timerId = setTimeout(() => setState(STATE.EXITED), duration);
}
return () => {
timerId && clearTimeout(timerId);
};
});
return [state, setState];
};
接下来,再把useTransitionState
钩子函数放在另一个钩子函数中useTransitionControl
,它为用户提供了两个回调,以便进入和退出状态之间进行切换。
const useTransitionControl = (duration) => {
const [state, setState] = useTransitionState(duration);
const enter = () => {
if (state !== STATE.EXITING) {
setState(STATE.ENTERING);
}
};
const exit = () => {
if (state !== STATE.ENTERING) {
setState(STATE.EXITING);
}
};
return [state, enter, exit];
};
现在我们使用它来实现一个具有渐隐渐现过渡效果的组件。
const defaultStyle = {
transition: "opacity 2000ms ease-in-out",
opacity: 0
};
const transitionStyles = {
entering: {
opacity: 1
},
entered: {
opacity: 1
},
exiting: {
opacity: 0
},
exited: {
opacity: 0
}
};
const FadeTransitionComponent = (props) => {
const [state, enter, exit] = useTransitionControl();
const style = {
...defaultStyle,
...(transitionStyles[state] ?? {})
};
return (
<>
<h2>State: {state}</h2>
<div className="alert" style={style}>
useTransitionState and useTransitionControl Hooks
</div>
<button className="btn btn__primary" onClick={enter}>
Enter
</button>
<button className="btn btn__danger" onClick={exit}>
Exit
</button>
</>
);
};
export default function App() {
return (
<div className="App">
<FadeTransitionComponent />
</div>
);
}
效果如下:
使用CSS animation
给元素添加动效
前面示例,我们看到的是在React中使用CSS的transition
给元素添加动画效果,另外,你可能在react-transition-group
中也发现了,也可以使用CSS的animation
给React中的组件(或元素)添加动效。
在React中,使用animation
给React元素添加动效和transition
类似,同样需要在DOM元素删除之前要有一个延迟时间,让我们能看到对应的动画效果。简单地说,需要一个时间控制器,让我们在React组件销毁之前就可以看到相应的动画效果。我们先来看一个简单的示例,也就是我们时常会碰到的一个效果,顶部导航栏淡入淡出的一个效果。
const HeaderBar = (props) => {
const { title = "Page Title", isHidden } = props;
const HEADERBAR_VISIBILITY_CLASSES = {
hidden: "hide",
visible: ""
};
const ANIMATION_CLASSES = {
fadeIn: "fade-in",
fadeOut: "fade-out"
};
// 默认状态使用淡入效果,对应的类名是visible
const [animationClass, setAnimationClass] = useState(
ANIMATION_CLASSES.fadeIn
);
const [headerBarHiddenClass, setHeaderBarHiddenClass] = useState(
HEADERBAR_VISIBILITY_CLASSES.visible
);
// 每当isHidden发生变化时,它就会运行
useEffect(() => {
let timerId = null;
if (isHidden) {
setAnimationClass(ANIMATION_CLASSES.fadeOut);
timerId = setTimeout(() => {
setHeaderBarHiddenClass(HEADERBAR_VISIBILITY_CLASSES.hidden);
}, 1000);
} else {
setAnimationClass(ANIMATION_CLASSES.fadeIn);
setHeaderBarHiddenClass(HEADERBAR_VISIBILITY_CLASSES.visible);
}
return () => {
clearTimeout(timerId);
};
}, [isHidden]);
return (
<div className={`header ${animationClass} ${headerBarHiddenClass}`}>
<h1>{title}</h1>
</div>
);
};
可以像下面这样引用HeaderBar
组件:
export default function App() {
const [isHidden, setIsHidden] = useState(false);
return (
<div className="App">
<HeaderBar isHidden={isHidden} title="欢迎来到W3cplus!" />
<button
className="button button__primary"
onClick={(e) => setIsHidden(!isHidden)}
>
Toggle HeaderBar
</button>
</div>
);
}
效果如下:
这个效果中我们通过改变div.header
的类名,fade-in
和fade-out
之间切换:
实现上面的效果,离不开下面的CSS代码:
.fade-in {
animation-duration: 1s;
animation-name: fadeIn;
}
.fade-out {
animation-duration: 1s;
animation-name: fadeOut;
}
.hide {
display: none;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translate3d(0, -100%, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
transform: translate3d(0, -100%, 0);
}
}
看到上面的代码,特别是@keyframes
定义的fadeIn
和fadeOut
特别的熟悉。他们其实就是Animate.css
中定义的动效。换句话说,我们对上面的代码稍做修改,就可以更灵活的使用Animate.css
定义的动效,甚至还可扩展任何@keyframes
定义的动效。
import React, { useEffect, useState } from "react";
import "./styles.css";
const HeaderBar = (props) => {
const {
title = "Page Title",
isHidden,
entrancesAnimationName,
exitsAnimationName
} = props;
const HEADERBAR_VISIBILITY_CLASSES = {
hidden: "hide",
visible: ""
};
const ANIMATION_CLASSES = {
entrancesAnimationName: `${entrancesAnimationName}`,
exitsAnimationName: `${exitsAnimationName}`
};
// 默认状态使用淡入效果,对应的类名是visible
const [animationClass, setAnimationClass] = useState(
ANIMATION_CLASSES.entrancesAnimationName
);
const [headerBarHiddenClass, setHeaderBarHiddenClass] = useState(
HEADERBAR_VISIBILITY_CLASSES.visible
);
// 每当isHidden发生变化时,它就会运行
useEffect(() => {
let timerId = null;
if (isHidden) {
setAnimationClass(ANIMATION_CLASSES.exitsAnimationName);
timerId = setTimeout(() => {
setHeaderBarHiddenClass(HEADERBAR_VISIBILITY_CLASSES.hidden);
}, 1000);
} else {
setAnimationClass(ANIMATION_CLASSES.entrancesAnimationName);
setHeaderBarHiddenClass(HEADERBAR_VISIBILITY_CLASSES.visible);
}
return () => {
clearTimeout(timerId);
};
}, [isHidden]);
return (
<div
className={`header animated ${animationClass} ${headerBarHiddenClass}`}
>
<h1>{title}</h1>
</div>
);
};
export default function App() {
const [isHidden, setIsHidden] = useState(false);
return (
<div className="App">
<HeaderBar
isHidden={isHidden}
title="欢迎来到W3cplus!"
entrancesAnimationName="bounceInDown"
exitsAnimationName="bounceOutUp"
/>
<button
className="button button__primary"
onClick={(e) => setIsHidden(!isHidden)}
>
Toggle HeaderBar
</button>
</div>
);
}
在CSS中添加bounceInDown
和bounceOutUp
对应的动画效果的代码:
:root {
--animate-duration: 1s;
}
.animated {
animation-duration: var(--animate-duration);
animation-fill-mode: both;
}
@keyframes bounceInDown {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(0, -3000px, 0) scaleY(3);
}
60% {
opacity: 1;
transform: translate3d(0, 25px, 0) scaleY(0.9);
}
75% {
transform: translate3d(0, -10px, 0) scaleY(0.95);
}
90% {
transform: translate3d(0, 5px, 0) scaleY(0.985);
}
to {
transform: translate3d(0, 0, 0);
}
}
.bounceInDown {
animation-name: bounceInDown;
}
@keyframes bounceOutUp {
20% {
transform: translate3d(0, -10px, 0) scaleY(0.985);
}
40%,
45% {
opacity: 1;
transform: translate3d(0, 20px, 0) scaleY(0.9);
}
to {
opacity: 0;
transform: translate3d(0, -2000px, 0) scaleY(3);
}
}
.bounceOutUp {
animation-name: bounceOutUp;
}
这个时候动效变成下面这样的:
用同样的方式,可以使用Animate.css
中定义所有动效。
构建animation
的React钩子函数
在React中同样可以使用React的Hooks为CSS的animation
构建相应的钩子函数。这个也有点类似于transition
的钩子函数,在介绍React构建transition
的钩子函数时,我们创建了useTransitionState
和useTransitionControl
两个钩子函数,构建animation
的钩子函数也有点类似。实现animation
效果,需要useAnimation
和useAnimationTimer
两个钩子函数,先来看useAnimationTimer
钩子函数:
import { useState, useEffect } from 'react';
// Usage
function App() {
const animation1 = useAnimation('elastic', 600, 0);
const animation2 = useAnimation('elastic', 600, 150);
const animation3 = useAnimation('elastic', 600, 300);
return (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Ball
innerStyle={{
marginTop: animation1 * 200 - 100
}}
/>
<Ball
innerStyle={{
marginTop: animation2 * 200 - 100
}}
/>
<Ball
innerStyle={{
marginTop: animation3 * 200 - 100
}}
/>
</div>
);
}
const Ball = ({ innerStyle }) => (
<div
style={{
width: 100,
height: 100,
marginRight: '40px',
borderRadius: '50px',
backgroundColor: '#4dd5fa',
...innerStyle
}}
/>
);
// Hook
function useAnimation(
easingName = 'linear',
duration = 500,
delay = 0
) {
const elapsed = useAnimationTimer(duration, delay);
const n = Math.min(1, elapsed / duration);
return easing[easingName](n);
}
// Some easing functions copied from:
// https://github.com/streamich/ts-easing/blob/master/src/index.ts
// Hardcode here or pull in a dependency
const easing = {
linear: n => n,
elastic: n =>
n * (33 * n * n * n * n - 106 * n * n * n + 126 * n * n - 67 * n + 15),
inExpo: n => Math.pow(2, 10 * (n - 1))
};
function useAnimationTimer(duration = 1000, delay = 0) {
const [elapsed, setTime] = useState(0);
useEffect(
() => {
let animationFrame, timerStop, start;
function onFrame() {
setTime(Date.now() - start);
loop();
}
function loop() {
animationFrame = requestAnimationFrame(onFrame);
}
function onStart() {
timerStop = setTimeout(() => {
cancelAnimationFrame(animationFrame);
setTime(Date.now() - start);
}, duration);
start = Date.now();
loop();
}
const timerDelay = setTimeout(onStart, delay);
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(animationFrame);
};
},
[duration, delay]
);
return elapsed;
}
特别声明:上面的代码来自于useHooks()中的
useAnimation
小结
在React中给组件添加动画效果,除了上面提供的一些方案之外,还有很多其他的方案以及一些优秀的动画库。这篇文章中我们从react-transition-group
开始到如何使用React Hooks专门为transition
和animation
定制相应的Hooks。不管是哪种方式,他们都解决同一个问题,那就是在React删除DOM之前先看到相应的动效。实现该功能主要是在组件对应的生命周期中使用setTimeout()
和定时器timerId
让DOM延迟销毁。如果你不想花时间了解他们,那可以考虑直接使用一些优秀的库,如果你感兴趣的话,我们在后续的教程中和大家一起聊聊如何使用这些库在React中给组件或元素添加动效。