この単純なことは簡単に達成できるはずですが、それでも私はそれがどれほど複雑かを考えて髪を引き出しています。
私がやりたいのは、Reactコンポーネントのマウントとアンマウントをアニメーション化することだけです。これまでに試したことと、各ソリューションが機能しない理由を以下に示します。
ReactCSSTransitionGroup
-私はCSSクラスをまったく使用していません。それはすべてJSスタイルなので、これは機能しません。ReactTransitionGroup
-この低レベルAPIは優れていますが、アニメーションの完了時にコールバックを使用する必要があるため、ここではCSSトランジションを使用するだけでは機能しません。アニメーションライブラリは常に存在し、次の点につながります。TransitionMotion
は非常に混乱し、必要なものに対して過度に複雑です。left: -10000px
)トリックを行うことはできますが、そのルートには行きたくありません。私はそれをハックだと思います、そしてwantコンポーネントをアンマウントして、それらがクリーンアップされ、DOMが乱雑にならないようにします。easyの何かを実装したい。マウント時に、一連のスタイルをアニメーション化します。アンマウント時に、同じ(または別の)スタイルのセットをアニメーション化します。できたまた、複数のプラットフォームで高いパフォーマンスを発揮する必要があります。
ここでレンガの壁にぶつかった。何かが欠けていて、これを行う簡単な方法がある場合は、お知らせください。
これは少し時間がかかりますが、このアニメーションを実現するためにすべてのネイティブイベントとメソッドを使用しました。 ReactCSSTransitionGroup
、ReactTransitionGroup
などはありません.
使用したもの
onTransitionEnd
イベントこれの仕組み
mounted
)に基づいて、デフォルトのスタイル(opacity: 0
)で要素をマウントしますcomponentDidMount
(componentWillReceiveProps
を使用してさらに更新します)を使用して、スタイル(opacity: 1
)をタイムアウト(非同期にする)に変更します。opacity: 0
)、onTransitionEnd
、DOMから要素をアンマウントします。サイクルを続けます。
コードに目を通すと、理解できます。明確化が必要な場合は、コメントを残してください。
お役に立てれば。
class App extends React.Component{
constructor(props) {
super(props)
this.transitionEnd = this.transitionEnd.bind(this)
this.mountStyle = this.mountStyle.bind(this)
this.unMountStyle = this.unMountStyle.bind(this)
this.state ={ //base css
show: true,
style :{
fontSize: 60,
opacity: 0,
transition: 'all 2s ease',
}
}
}
componentWillReceiveProps(newProps) { // check for the mounted props
if(!newProps.mounted)
return this.unMountStyle() // call outro animation when mounted prop is false
this.setState({ // remount the node when the mounted prop is true
show: true
})
setTimeout(this.mountStyle, 10) // call the into animation
}
unMountStyle() { // css for unmount animation
this.setState({
style: {
fontSize: 60,
opacity: 0,
transition: 'all 1s ease',
}
})
}
mountStyle() { // css for mount animation
this.setState({
style: {
fontSize: 60,
opacity: 1,
transition: 'all 1s ease',
}
})
}
componentDidMount(){
setTimeout(this.mountStyle, 10) // call the into animation
}
transitionEnd(){
if(!this.props.mounted){ // remove the node on transition end when the mounted prop is false
this.setState({
show: false
})
}
}
render() {
return this.state.show && <h1 style={this.state.style} onTransitionEnd={this.transitionEnd}>Hello</h1>
}
}
class Parent extends React.Component{
constructor(props){
super(props)
this.buttonClick = this.buttonClick.bind(this)
this.state = {
showChild: true,
}
}
buttonClick(){
this.setState({
showChild: !this.state.showChild
})
}
render(){
return <div>
<App onTransitionEnd={this.transitionEnd} mounted={this.state.showChild}/>
<button onClick={this.buttonClick}>{this.state.showChild ? 'Unmount': 'Mount'}</button>
</div>
}
}
ReactDOM.render(<Parent />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-with-addons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
Praneshの答えから得た知識を使用して、構成可能かつ再利用可能な代替ソリューションを思い付きました。
const AnimatedMount = ({ unmountedStyle, mountedStyle }) => {
return (Wrapped) => class extends Component {
constructor(props) {
super(props);
this.state = {
style: unmountedStyle,
};
}
componentWillEnter(callback) {
this.onTransitionEnd = callback;
setTimeout(() => {
this.setState({
style: mountedStyle,
});
}, 20);
}
componentWillLeave(callback) {
this.onTransitionEnd = callback;
this.setState({
style: unmountedStyle,
});
}
render() {
return <div
style={this.state.style}
onTransitionEnd={this.onTransitionEnd}
>
<Wrapped { ...this.props } />
</div>
}
}
};
使用法:
import React, { PureComponent } from 'react';
class Thing extends PureComponent {
render() {
return <div>
Test!
</div>
}
}
export default AnimatedMount({
unmountedStyle: {
opacity: 0,
transform: 'translate3d(-100px, 0, 0)',
transition: 'opacity 250ms ease-out, transform 250ms ease-out',
},
mountedStyle: {
opacity: 1,
transform: 'translate3d(0, 0, 0)',
transition: 'opacity 1.5s ease-out, transform 1.5s ease-out',
},
})(Thing);
最後に、別のコンポーネントのrender
メソッドで:
return <div>
<ReactTransitionGroup>
<Thing />
</ReactTransitionGroup>
</div>
私は仕事中にこの問題に対処しましたが、見た目は単純ですが、実際にはReactにはありません。次のようなものをレンダリングする通常のシナリオでは:
this.state.show ? {childen} : null;
this.state.show
が変更されると、子はすぐにマウント/アンマウントされます。
私が取った1つのアプローチは、ラッパーコンポーネントAnimate
を作成し、それを次のように使用することです
<Animate show={this.state.show}>
{childen}
</Animate>
現在、this.state.show
が変更されると、getDerivedStateFromProps(componentWillReceiveProps)
を使用して小道具の変更を認識し、アニメーションを実行する中間レンダーステージを作成できます。
子がマウントまたはアンマウントされるとき、Static Stageで始まります。
show
フラグの変更を検出したら、Prep Stageを入力し、height
やwidth
などの必要なプロパティをReactDOM.findDOMNode.getBoundingClientRect()
から計算します。
次に、Animate Stateと入力すると、CSSトランジションを使用して、高さ、幅、および不透明度を0から計算値(マウント解除の場合は0)に変更できます。
移行の最後に、onTransitionEnd
apiを使用して、Static
ステージに戻ります。
ステージがスムーズに移行する方法についてはさらに詳細がありますが、これは全体的な考えかもしれません。
興味のある方は、Reactライブラリを作成しました https://github.com/MingruiZhang/react-animate-mount 私のソリューションを共有します。フィードバックを歓迎します:)
react-transition-group
からTransition
を使用することが、おそらくマウント/アンマウントを追跡する最も簡単な方法だと思います。それは非常に柔軟です。使いやすさを示すためにいくつかのクラスを使用していますが、addEndListener
propを使用して独自のJSアニメーションを確実に接続することができます-GSAPを使用することもできました。
サンドボックス: https://codesandbox.io/s/k9xl9mkx2o
そして、これが私のコードです。
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { Transition } from "react-transition-group";
import styled from "styled-components";
const H1 = styled.h1`
transition: 0.2s;
/* Hidden init state */
opacity: 0;
transform: translateY(-10px);
&.enter,
&.entered {
/* Animate in state */
opacity: 1;
transform: translateY(0px);
}
&.exit,
&.exited {
/* Animate out state */
opacity: 0;
transform: translateY(-10px);
}
`;
const App = () => {
const [show, changeShow] = useState(false);
const onClick = () => {
changeShow(prev => {
return !prev;
});
};
return (
<div>
<button onClick={onClick}>{show ? "Hide" : "Show"}</button>
<Transition mountOnEnter unmountOnExit timeout={200} in={show}>
{state => {
let className = state;
return <H1 className={className}>Animate me</H1>;
}}
</Transition>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
コンポーネントのアンマウントフェーズを遅らせるために、新しいフックAPI(TypeScriptを使用)、 この投稿に基づいて を使用した私のソリューションを次に示します。
function useDelayUnmount(isMounted: boolean, delayTime: number) {
const [ shouldRender, setShouldRender ] = useState(false);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
if (isMounted && !shouldRender) {
setShouldRender(true);
}
else if(!isMounted && shouldRender) {
timeoutId = setTimeout(
() => setShouldRender(false),
delayTime
);
}
return () => clearTimeout(timeoutId);
});
return shouldRender;
}
使用法:
const Parent: React.FC = () => {
const [ isMounted, setIsMounted ] = useState(true);
const shouldRenderChild = useDelayUnmount(isMounted, 500);
const mountedStyle = {opacity: 1, transition: "opacity 500ms ease-in"};
const unmountedStyle = {opacity: 0, transition: "opacity 500ms ease-in"};
const handleToggleClicked = () => {
setIsMounted(!isMounted);
}
return (
<>
{shouldRenderChild &&
<Child style={isMounted ? mountedStyle : unmountedStyle} />}
<button onClick={handleToggleClicked}>Click me!</button>
</>
);
}
CodeSandbox リンク。
OnMountで遷移を含む別のclassNameを追加し、onUnMountでそのクラス名を削除するとどうなりますか?
react-move を使用すると、入力遷移と終了遷移のアニメーションがはるかに簡単になります。
反応モーションを検討している人にとっては、単一のコンポーネントをマウントおよびアンマウントするときにアニメーション化すると、セットアップが圧倒される場合があります。
react-motion-ui-packと呼ばれるライブラリがあり、このプロセスを開始するのがはるかに簡単になります。これは、react-motionのラッパーです。つまり、ライブラリからすべてのメリットを得ることができます(つまり、アニメーションを中断し、複数のアンマウントを同時に行うことができます)。
使用法:
import Transition from 'react-motion-ui-pack'
<Transition
enter={{ opacity: 1, translateX: 0 }}
leave={{ opacity: 0, translateX: -100 }}
component={false}
>
{ this.state.show &&
<div key="hello">
Hello
</div>
}
</Transition>
Enterは、コンポーネントの最終状態を定義します。 leaveは、コンポーネントがアンマウントされたときに適用されるスタイルです。
UIパックを数回使用すると、react-motionライブラリーはもう手ごわくないかもしれません。
ここで私の2セント:彼のソリューションのための@deckeleに感謝します。私のソリューションは彼に基づいています。それはステートフルのコンポーネントバージョンであり、完全に再利用可能です。
ここに私のサンドボックス: https://codesandbox.io/s/302mkm1m 。
ここに私のsnippet.js:
import ReactDOM from "react-dom";
import React, { Component } from "react";
import style from "./styles.css";
class Tooltip extends Component {
state = {
shouldRender: false,
isMounted: true,
}
shouldComponentUpdate(nextProps, nextState) {
if (this.state.shouldRender !== nextState.shouldRender) {
return true
}
else if (this.state.isMounted !== nextState.isMounted) {
console.log("ismounted!")
return true
}
return false
}
displayTooltip = () => {
var timeoutId;
if (this.state.isMounted && !this.state.shouldRender) {
this.setState({ shouldRender: true });
} else if (!this.state.isMounted && this.state.shouldRender) {
timeoutId = setTimeout(() => this.setState({ shouldRender: false }), 500);
() => clearTimeout(timeoutId)
}
return;
}
mountedStyle = { animation: "inAnimation 500ms ease-in" };
unmountedStyle = { animation: "outAnimation 510ms ease-in" };
handleToggleClicked = () => {
console.log("in handleToggleClicked")
this.setState((currentState) => ({
isMounted: !currentState.isMounted
}), this.displayTooltip());
};
render() {
var { children } = this.props
return (
<main>
{this.state.shouldRender && (
<div className={style.tooltip_wrapper} >
<h1 style={!(this.state.isMounted) ? this.mountedStyle : this.unmountedStyle}>{children}</h1>
</div>
)}
<style>{`
@keyframes inAnimation {
0% {
transform: scale(0.1);
opacity: 0;
}
60% {
transform: scale(1.2);
opacity: 1;
}
100% {
transform: scale(1);
}
}
@keyframes outAnimation {
20% {
transform: scale(1.2);
}
100% {
transform: scale(0);
opacity: 0;
}
}
`}
</style>
</main>
);
}
}
class App extends Component{
render(){
return (
<div className="App">
<button onClick={() => this.refs.tooltipWrapper.handleToggleClicked()}>
click here </button>
<Tooltip
ref="tooltipWrapper"
>
Here a children
</Tooltip>
</div>
)};
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
これは、ロードスピナーを作成しながら、2019年にこれをどのように解決したかです。 React機能コンポーネントを使用しています。
親Appコンポーネントと子Spinnerコンポーネントがあります。
Appは、アプリがロードされているかどうかの状態を持ちます。アプリがロードされると、Spinnerが正常にレンダリングされます。アプリが読み込まれていない場合(isLoading
がfalse)Spinnerはprop shouldUnmount
でレンダリングされます。
App.js:
import React, {useState} from 'react';
import Spinner from './Spinner';
const App = function() {
const [isLoading, setIsLoading] = useState(false);
return (
<div className='App'>
{isLoading ? <Spinner /> : <Spinner shouldUnmount />}
</div>
);
};
export default App;
Spinnerは非表示かどうかの状態を持ちます。最初は、デフォルトの小道具と状態でSpinnerが通常通りレンダリングされます。 Spinner-fadeIn
クラスはフェードインします。Spinnerがprop shouldUnmount
を受け取ると、代わりにSpinner-fadeOut
クラスでレンダリングし、フェードアウトします。
ただし、フェードアウト後にコンポーネントをアンマウントすることも必要でした。
この時点で、上記の@ pranesh-raviのソリューションに似たonAnimationEnd
React合成イベントを使用しようとしましたが、機能しませんでした。代わりに、setTimeout
を使用して、アニメーションと同じ長さの遅延で状態を非表示に設定しました。 SpinnerはisHidden === true
で遅延後に更新され、何もレンダリングされません。
ここで重要なのは、親は子をアンマウントせず、子にいつアンマウントするかを指示し、子はアンマウントするビジネスの面倒を見てからアンマウントします。
Spinner.js:
import React, {useState} from 'react';
import './Spinner.css';
const Spinner = function(props) {
const [isHidden, setIsHidden] = useState(false);
if(isHidden) {
return null
} else if(props.shouldUnmount) {
setTimeout(setIsHidden, 500, true);
return (
<div className='Spinner Spinner-fadeOut' />
);
} else {
return (
<div className='Spinner Spinner-fadeIn' />
);
}
};
export default Spinner;
Spinner.css:
.Spinner {
position: fixed;
display: block;
z-index: 999;
top: 50%;
left: 50%;
margin: -40px 0 0 -20px;
height: 40px;
width: 40px;
border: 5px solid #00000080;
border-left-color: #bbbbbbbb;
border-radius: 40px;
}
.Spinner-fadeIn {
animation:
rotate 1s linear infinite,
fadeIn .5s linear forwards;
}
.Spinner-fadeOut {
animation:
rotate 1s linear infinite,
fadeOut .5s linear forwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
また、単一コンポーネントのアニメーションも急務でした。 React Motionを使用して疲れましたが、このような些細な問題のために髪を引っ張っていました。いくつかのグーグル検索の後、私は彼らのgitリポジトリでこの投稿に出会いました。それが誰かを助けることを願っています。
参照元およびクレジット 。これは今のところ私にとってはうまくいきます。私のユースケースは、ロードおよびアンロードの場合にアニメーション化およびアンマウントするモーダルでした。
class Example extends React.Component {
constructor() {
super();
this.toggle = this.toggle.bind(this);
this.onRest = this.onRest.bind(this);
this.state = {
open: true,
animating: false,
};
}
toggle() {
this.setState({
open: !this.state.open,
animating: true,
});
}
onRest() {
this.setState({ animating: false });
}
render() {
const { open, animating } = this.state;
return (
<div>
<button onClick={this.toggle}>
Toggle
</button>
{(open || animating) && (
<Motion
defaultStyle={open ? { opacity: 0 } : { opacity: 1 }}
style={open ? { opacity: spring(1) } : { opacity: spring(0) }}
onRest={this.onRest}
>
{(style => (
<div className="box" style={style} />
))}
</Motion>
)}
</div>
);
}
}