web-dev-qa-db-fra.com

React - animer le montage et le démontage d'un seul composant

Quelque chose d'aussi simple devrait être facilement accompli, et pourtant je me tire les cheveux de leur complication.

Tout ce que je veux faire, c'est animer le montage et le démontage d'un composant React, c'est tout. Voici ce que j'ai essayé jusqu'à présent et pourquoi chaque solution ne fonctionne pas:

  1. ReactCSSTransitionGroup - Je n'utilise pas du tout de classes CSS, ce sont tous des styles JS, donc cela ne fonctionnera pas.
  2. ReactTransitionGroup - Cette API de niveau inférieur est géniale, mais vous devez utiliser un rappel lorsque l'animation est terminée. L'utilisation de transitions CSS ne fonctionnera donc pas ici. Il y a toujours des bibliothèques d'animation, ce qui mène au point suivant:
  3. GreenSock - La licence est trop restrictive pour une utilisation professionnelle IMO.
  4. React Motion - Cela semble génial, mais TransitionMotion est extrêmement déroutant et trop compliqué pour ce dont j'ai besoin.
  5. Bien sûr, je peux juste faire des trucs comme Material UI, où les éléments sont restitués mais restent cachés (left: -10000px) mais je préfère ne pas aller dans cette voie. Je le considère comme un hacky et je veux démonter mes composants pour qu’ils soient nettoyés et ne surchargent pas le DOM.

Je veux quelque chose qui est facile à mettre en œuvre . Sur le montage, animez un ensemble de styles; sur démonter, animer le même (ou un autre) ensemble de styles. Terminé. Il doit également être performant sur plusieurs plates-formes.

J'ai heurté un mur de briques ici. Si je manque quelque chose et qu'il existe un moyen facile de le faire, faites le moi savoir.

71
ffxsam

C'est un peu long, mais j'ai utilisé tous les événements et méthodes natifs pour réaliser cette animation. Non ReactCSSTransitionGroup, ReactTransitionGroup et etc.

Ce que j'ai utilisé

  • Réagissez les méthodes du cycle de vie
  • onTransitionEnd événement

Comment ça marche

  • Monter l'élément en fonction de l'hélice de montage passée (mounted) et avec le style par défaut (opacity: 0)
  • Après le montage ou la mise à jour, utilisez componentDidMount (componentWillReceiveProps pour les mises à jour ultérieures) pour modifier le style (opacity: 1) avec un délai d'attente (pour le rendre asynchrone).
  • Pendant le démontage, passez un accessoire au composant pour l'identifier, modifiez à nouveau le style (opacity: 0), onTransitionEnd, supprimez l'élément du DOM du démontage.

Continuez le cycle.

Passez par le code, vous comprendrez. Si des éclaircissements sont nécessaires, laissez un commentaire.

J'espère que cela t'aides.

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>
86
Pranesh Ravi

En utilisant les connaissances acquises grâce à la réponse de Pranesh, j'ai proposé une autre solution configurable et réutilisable:

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

Usage:

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

Et enfin, dans la méthode render d'un autre composant:

return <div>
  <ReactTransitionGroup>
    <Thing />
  </ReactTransitionGroup>
</div>
14
ffxsam

J'ai contré ce problème pendant mon travail, et aussi simple que cela paraisse, ce n'est vraiment pas dans React. Dans un scénario normal où vous rendez quelque chose comme:

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

lorsque this.state.show change, les enfants sont immédiatement montés/démontés.

Une approche que j’ai choisie consiste à créer un composant wrapper Animate et à l’utiliser comme

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

maintenant, alors que this.state.show change, nous pouvons percevoir les changements de propriétés avec getDerivedStateFromProps(componentWillReceiveProps) et créer des étapes de rendu intermédiaires pour réaliser des animations.

A stage cycle might look like this

Nous commençons par Stage statique lorsque les enfants sont montés ou non.

Une fois que nous détectons les changements de drapeau show, nous entrons Prep Stage où nous calculons les propriétés nécessaires comme height et width de ReactDOM.findDOMNode.getBoundingClientRect().

Ensuite, en entrant État animé , nous pouvons utiliser la transition css pour modifier la hauteur, la largeur et l'opacité de 0 aux valeurs calculées (ou à 0 si démontées).

En fin de transition, nous utilisons onTransitionEnd api pour revenir au stade Static.

Il y a beaucoup plus de détails sur la manière dont les étapes sont transférées sans heurt, mais cela pourrait être une idée générale :)

Si vous êtes intéressé, j'ai créé une bibliothèque React https://github.com/MingruiZhang/react-animate-mount pour partager ma solution. Tout commentaire bienvenu :)

8
Mingrui Zhang

Voici ma solution utilisant la nouvelle API de crochets (avec TypeScript), basé sur cet article , pour retarder la phase de démontage du composant:

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

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

4
deckele

Je pense que l’utilisation de Transition de react-transition-group est probablement le moyen le plus simple de suivre le montage/démontage. C'est incroyablement flexible. J'utilise certaines classes pour montrer à quel point il est facile à utiliser, mais vous pouvez certainement connecter vos propres animations JS en utilisant addEndListener prop - avec lequel j'ai eu beaucoup de chance en utilisant GSAP.

Sandbox: https://codesandbox.io/s/k9xl9mkx2o

Et voici mon code.

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

Que se passe-t-il si onMount vous ajoutez un autre className, qui contient la transition, et onUnMount vous supprimez ce nom de classe?

1
Frazer Kirkman

L'animation des transitions d'entrée et de sortie est beaucoup plus facile avec react-move .

exemple sur codesandbox

1
delimited

Pour ceux qui envisagent de réagir à un mouvement, animer un seul composant lorsqu’il monte et s’enleve peut s’avérer fastidieux.

Il existe une bibliothèque appelée react-motion-ui-pack qui rend ce processus beaucoup plus facile à démarrer. C'est un wrapper autour de react-motion, ce qui signifie que vous bénéficiez de tous les avantages de la bibliothèque (c'est-à-dire que vous pouvez interrompre l'animation, si plusieurs démontages sont effectués en même temps).

Utilisation:

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 définit ce que l'état final du composant doit être; congé est le style appliqué lorsque le composant est démonté.

Vous constaterez peut-être qu'une fois que vous avez utilisé le pack d'interface utilisateur à plusieurs reprises, la bibliothèque react-motion n'est peut-être plus aussi intimidante.

1
Björn Holdt

Voici mes 2cents: merci à @deckele pour sa solution. Ma solution est basée sur la sienne, c'est la version du composant stateful, entièrement réutilisable.

voici mon bac à sable: https://codesandbox.io/s/302mkm1m .

voici mon 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

Voici comment j'ai résolu ce problème en 2019 en faisant une spinner de chargement. J'utilise React composants fonctionnels.

J'ai un parent App composant qui a un enfant Spinner.

App indique si l'application est en cours de chargement ou non. Lorsque l'application est en cours de chargement, Spinner est restitué normalement. Lorsque l'application ne se charge pas (isLoading est false) Spinner est rendu avec l'accessoire 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 a l'état de savoir si c'est caché ou non. Au début, avec les accessoires et l’état par défaut, Spinner est restitué normalement. La classe Spinner-fadeIn l'anime en fondu. Lorsque Spinner reçoit l'accessoire shouldUnmount il restitue avec la classe Spinner-fadeOut, l'animant en fondu.

Cependant, je souhaitais également que le composant se démonte après la sortie en fondu.

À ce stade, j'ai essayé d'utiliser l'événement synthétique onAnimationEnd React, similaire à la solution de @ pranesh-ravi ci-dessus, mais cela n'a pas fonctionné. Au lieu de cela, j'ai utilisé setTimeout pour définir l'état caché avec un délai de la même longueur que l'animation. Spinner sera mis à jour après le délai avec isHidden === true, et rien ne sera rendu.

La clé ici est que le parent ne démonte pas l'enfant, il dit à l'enfant quand le démonter et l'enfant se démonte lui-même après avoir pris soin de son entreprise de démontage.

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

J'avais aussi un besoin urgent d'animation à un seul composant. J'en avais marre d'utiliser React Motion, mais je me tirais les cheveux pour une question aussi triviale. Après quelques recherches sur Google, je suis tombé sur ce message sur leur dépôt Git. J'espère que ça aide quelqu'un ..

référencé depuis et également le crédit . Cela fonctionne pour moi à partir de maintenant. Mon cas d'utilisation était un modal pour animer et démonter en cas de chargement et de déchargement.

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