React - 动画装载和卸载单个组件

ffx*_*sam 73 animation css-animations greensock reactjs react-motion

这个简单的东西应该很容易实现,但是我把头发拉出来是多么复杂.

我想做的就是动画安装和卸载React组件,就是这样.这是我到目前为止所尝试的以及为什么每个解决方案都不起作用:

  1. ReactCSSTransitionGroup - 我根本不使用CSS类,它是所有JS样式,所以这不起作用.
  2. ReactTransitionGroup - 这个较低级别的API很棒,但它要求你在动画完成时使用回调,所以只使用CSS过渡在这里不起作用.总有动画库,这导致下一点:
  3. GreenSock - 许可证对于商业用途IMO来说过于严格.
  4. React Motion - 这看起来很棒,但是TransitionMotion对于我需要的东西来说却非常混乱和过于复杂.
  5. 当然我可以像Material UI那样做一些技巧,其中元素被渲染但保持隐藏(left: -10000px)但我宁愿不去那条路线.我认为它很hacky,我希望卸载我的组件,以便清理它们并且不会使DOM混乱.

我想要一些易于实现的东西.在mount上,动画一组样式; 在卸载时,为相同(或另一组)样式设置动画.完成.它还必须在多个平台上具有高性能.

我在这里碰到了一堵砖墙.如果我遗漏了一些东西并且有一个简单的方法可以做到这一点,请告诉我.

Pra*_*avi 89

这有点长,但我已经使用了所有本机事件和方法来实现这个动画.不ReactCSSTransitionGroup,ReactTransitionGroup等等

我用过的东西

  • 反应生命周期方法
  • onTransitionEnd 事件

这是如何工作的

  • 基于mount prop(mounted)和默认样式(opacity: 0)挂载元素
  • 安装或更新后,使用componentDidMount(componentWillReceiveProps进一步更新opacity: 1)使用超时更改样式()(使其为异步).
  • 在卸载期间,将prop传递给组件以标识卸载,再次更改样式(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'))
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

  • 顺便说一下,我认为你的代码中存在错误.在你的`Parent`组件中,你引用`this.transitionEnd` (7认同)
  • 你怎么知道它做了什么,文档没有解释任何东西。另一个问题:你怎么知道`componentWillReceiveProps`可以返回一些东西?我在哪里可以阅读更多相关信息? (2认同)
  • 虽然这不会卸载“App”,但“App”只是知道何时不渲染任何内容。 (2认同)

ffx*_*sam 14

利用从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>
    }
  }
};
Run Code Online (Sandbox Code Playgroud)

用法:

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);
Run Code Online (Sandbox Code Playgroud)

最后,在另一个组件的render方法中:

return <div>
  <ReactTransitionGroup>
    <Thing />
  </ReactTransitionGroup>
</div>
Run Code Online (Sandbox Code Playgroud)


Min*_*ang 8

我在工作期间反驳了这个问题,看起来很简单,它实际上并不在React中.在正常情况下,您可以呈现如下内容:

this.state.show ? {childen} : null;
Run Code Online (Sandbox Code Playgroud)

作为this.state.show改变,孩子们立即安装/卸载.

我采取的一种方法是创建一个包装器组件Animate并使用它

<Animate show={this.state.show}>
  {childen}
</Animate>
Run Code Online (Sandbox Code Playgroud)

现在作为this.state.show变化,我们可以感知道具变化,getDerivedStateFromProps(componentWillReceiveProps)并创建中间渲染阶段来执行动画.

舞台周期可能如下所示

我们从安装或卸载儿童时开始使用静态舞台.

一旦我们检测到show标志更改,我们进入准备阶段,我们计算必要的属性,如heightwidthReactDOM.findDOMNode.getBoundingClientRect().

然后输入Animate State我们可以使用css过渡将高度,宽度和不透明度从0更改为计算值(如果卸载则更改为0).

在过渡结束时,我们使用onTransitionEndapi改回 Static舞台.

关于阶段如何顺利转移的更多细节,但这可能是整体想法:)

如果有兴趣的话,我创建了一个React库https://github.com/MingruiZhang/react-animate-mount来分享我的解决方案.任何反馈欢迎:)

  • @MingruiZhang 很高兴看到您积极地接受了评论并改进了您的答案。看的很爽。干得好。 (3认同)

dec*_*ele 7

这是基于此帖子的使用新的hooks API(带有TypeScript)的解决方案,用于延迟组件的卸载阶段:

function useDelayUnmount(isMounted: boolean, delayTime: number) {
    const [ shouldRender, setShouldRender ] = useState(false);

    useEffect(() => {
        let timeoutId: number;
        if (isMounted && !shouldRender) {
            setShouldRender(true);
        }
        else if(!isMounted && shouldRender) {
            timeoutId = setTimeout(
                () => setShouldRender(false), 
                delayTime
            );
        }
        return () => clearTimeout(timeoutId);
    }, [isMounted, delayTime, shouldRender]);
    return shouldRender;
}
Run Code Online (Sandbox Code Playgroud)

用法:

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>
        </>
    );
}
Run Code Online (Sandbox Code Playgroud)

CodeSandbox链接。

  • 优雅的解决方案,如果您添加了一些评论,那就太好了:) (4认同)
  • 要修复 TypeScript 错误“Type 'Timeout' is not assignable to type 'number'.”,请在“setTimeout”前面添加“window.”。 (3认同)
  • @Webwoman 感谢您的评论。我无法重新创建您报告的“NodeJS 超时”问题,请参阅答案下方的 CodeSandbox 链接。关于 TypeScript,我个人更喜欢使用它而不是 JavaScript,尽管两者当然都是可行的。 (2认同)
  • 我一直在寻找像这样的干净的钩子解决方案,这个实现是我见过的最干净的解决方案。只需一个简单的钩子。 (2认同)

小智 7

我认为使用Transitionfromreact-transition-group可能是跟踪安装/卸载的最简单方法。它非常灵活。我正在使用一些类来展示它的易用性,但是您绝对可以使用addEndListenerprop连接自己的 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);
Run Code Online (Sandbox Code Playgroud)

  • 如果您使用样式组件,您可以简单地将 `show`prop 传递给 `H1` 并在样式组件内执行所有逻辑。就像...`动画:${({ show }) =&gt; show ? EntryKeyframes : exitKeyframes} 向前缓出 300 毫秒;` (3认同)

ton*_*y95 6

成帧运动

从 npm 安装 framer-motion。

import { motion, AnimatePresence } from "framer-motion"

export const MyComponent = ({ isVisible }) => (
  <AnimatePresence>
    {isVisible && (
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
      />
    )}
  </AnimatePresence>
)
Run Code Online (Sandbox Code Playgroud)