React - animate mount and unmount of a single component

前端 未结 16 1271
南笙
南笙 2020-12-22 16:34

Something this simple should be easily accomplished, yet I\'m pulling my hair out over how complicated it is.

All I want to do is animate the mounting & unmounti

相关标签:
16条回答
  • 2020-12-22 17:02

    For those considering react-motion, animating a single component when it mounts and unmounts can be overwhelming to set up.

    There's a library called react-motion-ui-pack that makes this process a lot easier to start with. It's a wrapper around react-motion, which means you get all the benefits from the library (i.e. you are able to interrupt the animation, have multiple unmounts happen at the same time).

    Usage:

    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 defines what the end state of the component should be; leave is the style that is applied when the component is unmounted.

    You might find that once you have used the UI pack a couple of times, the react-motion library might not be as daunting anymore.

    0 讨论(0)
  • 2020-12-22 17:05

    Animating enter and exit transitions is much easier with react-move.

    example on codesandbox

    0 讨论(0)
  • 2020-12-22 17:07

    Here's how I solved this in 2019, while making a loading spinner. I'm using React functional components.

    I have a parent App component that has a child Spinner component.

    App has state for whether the app is loading or not. When the app is loading, Spinner is rendered normally. When the app is not loading (isLoading is false) Spinner is rendered with the 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 has state for whether it's hidden or not. In the beginning, with default props and state, Spinner is rendered normally. The Spinner-fadeIn class animates it fading in. When Spinner receives the prop shouldUnmount it renders with the Spinner-fadeOut class instead, animating it fading out.

    However I also wanted the component to unmount after fading out.

    At this point I tried using the onAnimationEnd React synthetic event, similar to @pranesh-ravi's solution above, but it didn't work. Instead I used setTimeout to set the state to hidden with a delay the same length as the animation. Spinner will update after the delay with isHidden === true, and nothing will be rendered.

    The key here is that the parent doesn't unmount the child, it tells the child when to unmount, and the child unmounts itself after it takes care of its unmounting business.

    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 讨论(0)
  • 2020-12-22 17:10

    Here is my solution using the new hooks API (with TypeScript), based on this post, for delaying the component's unmount phase:

    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;
    }
    

    Usage:

    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 link.

    0 讨论(0)
  • 2020-12-22 17:10

    Here my 2cents: thanks to @deckele for his solution. My solution is based on his, it's the stateful's component version, fully reusable.

    here my sandbox: https://codesandbox.io/s/302mkm1m.

    here my 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 讨论(0)
  • 2020-12-22 17:12

    Framer motion

    Install framer-motion from npm.

    import { motion, AnimatePresence } from "framer-motion"
    
    export const MyComponent = ({ isVisible }) => (
      <AnimatePresence>
        {isVisible && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          />
        )}
      </AnimatePresence>
    )
    
    0 讨论(0)
提交回复
热议问题