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

IT技术 animation reactjs css-animations gsap react-motion
2021-03-24 15:43:08

这么简单的事情应该很容易完成,但我对它的复杂程度感到困惑。

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

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

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

我在这里撞到了一堵砖墙。如果我遗漏了什么并且有一种简单的方法可以做到这一点,请告诉我。

6个回答

这有点冗长,但我已使用所有本机事件和方法来实现此动画。ReactCSSTransitionGroupReactTransitionGroup等等。

我用过的东西

  • react生命周期方法
  • onTransitionEnd 事件

这是如何工作的

  • 根据传递的挂载props(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'))
<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>

@ffxsam onTransitionEnd 是原生 JavaScript 事件。你可以谷歌一下。facebook.github.io/react/docs/...会给你一个关于 componentWillReceiveProps 的想法。
2021-05-31 15:43:08
@ffxsam facebook.github.io/react/docs/events.html它在过渡事件之下。
2021-06-03 15:43:08
你怎么知道它做了什么,文档没有解释任何东西。另一个问题:你怎么知道componentWillReceiveProps可以返回一些东西?我在哪里可以阅读更多相关信息?
2021-06-09 15:43:08
顺便说一句,我认为您的代码中有错误。在您的Parent组件中,您引用this.transitionEnd
2021-06-15 15:43:08
App虽然这不会卸载,但App只是知道什么时候不渲染任何东西。
2021-06-20 15:43:08

这是我使用新钩子 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;
}

用法:

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链接。

还有为什么要使用 typecrypt 的扩展,因为它在 javascript 的扩展中运行良好?
2021-05-27 15:43:08
@Webwoman 感谢您的评论。我无法用“NodeJS 超时”重新创建您报告的问题,请参阅答案下方的我的 CodeSandbox 链接。关于 TypeScript,我个人更喜欢使用它而不是 JavaScript,尽管两者当然都是可行的。
2021-05-27 15:43:08
优雅的解决方案,如果您添加了一些评论,那就太好了:)
2021-06-12 15:43:08
您的控制台也返回“找不到命名空间 NodeJS 超时”
2021-06-14 15:43:08

使用从 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>
如何componentWillLeave()componentWillEnter()获取调用的AnimatedMount
2021-05-25 15:43:08
这不可能工作,因为这些方法没有被调用,而且正如预期的那样,它不起作用。
2021-05-26 15:43:08
我认为这个答案已经过时了……这个例子似乎在后台需要ReactTransitionGroup,它曾经是 React 的一部分,现在有一个单独的包。但该包还提供了TransitionCSSTransition,这在这里更合适。
2021-06-06 15:43:08
对我不起作用,这里是我的沙箱:codesandbox.io/s/p9m5625v6m
2021-06-14 15:43:08
你如何挂载/卸载@ffxsam?
2021-06-17 15:43:08

我在工作中解决了这个问题,虽然看起来很简单,但在 React 中确实没有。在正常情况下,您渲染如下内容:

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

随着this.state.show变化,孩子们会立即安装/卸载。

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

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

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

阶段周期可能如下所示

当孩子被装载或卸载时,我们从静态阶段开始

一旦我们检测到show标志的变化,我们就会进入准备阶段,在那里我们计算必要的属性,比如heightwidthfrom ReactDOM.findDOMNode.getBoundingClientRect()

然后进入动画状态,我们可以使用 css transition 将高度、宽度和不透明度从 0 更改为计算值(如果卸载则更改为 0)。

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

关于阶段如何顺利转移还有更多细节,但这可能是总体思路:)

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

感谢您的反馈,抱歉之前的粗鲁回答。我在答案中添加了更多细节和图表,希望这对其他人更有帮助。
2021-05-28 15:43:08
@MingruiZhang 很高兴看到您积极地接受了评论并改进了您的答案。看的很爽。干得好。
2021-06-11 15:43:08

我认为使用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);
此解决方案不适合我。如果我将过渡/超时时间设置为 2s/2000ms,我可以清楚地看到,当触发输入动画时,元素会保持隐藏 2 秒,然后才过渡 2 秒。
2021-05-25 15:43:08
如果您使用样式组件,您可以简单地将showprop传递H1样式组件并在其中执行所有逻辑。喜欢...animation: ${({ show }) => show ? entranceKeyframes : exitKeyframes} 300ms ease-out forwards;
2021-06-15 15:43:08