web-dev-qa-db-ja.com

React-単一コンポーネントのマウントとアンマウントをアニメーション化します

この単純なことは簡単に達成できるはずですが、それでも私はそれがどれほど複雑かを考えて髪を引き出しています。

私がやりたいのは、Reactコンポーネントのマウントとアンマウントをアニメーション化することだけです。これまでに試したことと、各ソリューションが機能しない理由を以下に示します。

  1. ReactCSSTransitionGroup-私はCSSクラスをまったく使用していません。それはすべてJSスタイルなので、これは機能しません。
  2. ReactTransitionGroup-この低レベルAPIは優れていますが、アニメーションの完了時にコールバックを使用する必要があるため、ここではCSSトランジションを使用するだけでは機能しません。アニメーションライブラリは常に存在し、次の点につながります。
  3. GreenSock-ライセンスは、ビジネスで使用するIMOには制限が厳しすぎます。
  4. React Motion-これは素晴らしいように思えますが、TransitionMotionは非常に混乱し、必要なものに対して過度に複雑です。
  5. もちろん、マテリアルUIのように、要素はレンダリングされますが、隠されたまま(left: -10000px)トリックを行うことはできますが、そのルートには行きたくありません。私はそれをハックだと思います、そしてwantコンポーネントをアンマウントして、それらがクリーンアップされ、DOMが乱雑にならないようにします。

easyの何かを実装したい。マウント時に、一連のスタイルをアニメーション化します。アンマウント時に、同じ(または別の)スタイルのセットをアニメーション化します。できたまた、複数のプラットフォームで高いパフォーマンスを発揮する必要があります。

ここでレンガの壁にぶつかった。何かが欠けていて、これを行う簡単な方法がある場合は、お知らせください。

64
ffxsam

これは少し時間がかかりますが、このアニメーションを実現するためにすべてのネイティブイベントとメソッドを使用しました。 ReactCSSTransitionGroupReactTransitionGroupなどはありません.

使用したもの

  • Reactライフサイクルメソッド
  • onTransitionEndイベント

これの仕組み

  • 渡されたマウント支柱(mounted)に基づいて、デフォルトのスタイル(opacity: 0)で要素をマウントします
  • マウントまたは更新後、componentDidMountcomponentWillReceivePropsを使用してさらに更新します)を使用して、スタイル(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>
78
Pranesh Ravi

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>
14
ffxsam

私は仕事中にこの問題に対処しましたが、見た目は単純ですが、実際にはReactにはありません。次のようなものをレンダリングする通常のシナリオでは:

this.state.show ? {childen} : null;

this.state.showが変更されると、子はすぐにマウント/アンマウントされます。

私が取った1つのアプローチは、ラッパーコンポーネントAnimateを作成し、それを次のように使用することです

<Animate show={this.state.show}>
  {childen}
</Animate>

現在、this.state.showが変更されると、getDerivedStateFromProps(componentWillReceiveProps)を使用して小道具の変更を認識し、アニメーションを実行する中間レンダーステージを作成できます。

A stage cycle might look like this

子がマウントまたはアンマウントされるとき、Static Stageで始まります。

showフラグの変更を検出したら、Prep Stageを入力し、heightwidthなどの必要なプロパティをReactDOM.findDOMNode.getBoundingClientRect()から計算します。

次に、Animate Stateと入力すると、CSSトランジションを使用して、高さ、幅、および不透明度を0から計算値(マウント解除の場合は0)に変更できます。

移行の最後に、onTransitionEnd apiを使用して、Staticステージに戻ります。

ステージがスムーズに移行する方法についてはさらに詳細がありますが、これは全体的な考えかもしれません。

興味のある方は、Reactライブラリを作成しました https://github.com/MingruiZhang/react-animate-mount 私のソリューションを共有します。フィードバックを歓迎します:)

7
Mingrui Zhang

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);
2
Shalanah

コンポーネントのアンマウントフェーズを遅らせるために、新しいフック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 リンク。

2
deckele

OnMountで遷移を含む別のclassNameを追加し、onUnMountでそのクラス名を削除するとどうなりますか?

1
Frazer Kirkman

react-move を使用すると、入力遷移と終了遷移のアニメーションがはるかに簡単になります。

codesandboxの例

1
delimited

反応モーションを検討している人にとっては、単一のコンポーネントをマウントおよびアンマウントするときにアニメーション化すると、セットアップが圧倒される場合があります。

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ライブラリーはもう手ごわくないかもしれません。

1
Björn Holdt

ここで私の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);
0
Webwoman

これは、ロードスピナーを作成しながら、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を使用して、アニメーションと同じ長さの遅延で状態を非表示に設定しました。 SpinnerisHidden === 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);
    }
}
0
mjw

また、単一コンポーネントのアニメーションも急務でした。 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>
    );
  }
}
0
Rahul Singh