React - anima a montagem e desmontagem de um único componente

97

Algo tão simples deve ser facilmente realizado, mas estou puxando meu cabelo por causa do quão complicado é.

Tudo que eu quero fazer é animar a montagem e desmontagem de um componente React, é isso. Aqui está o que tentei até agora e por que cada solução não funciona:

  1. ReactCSSTransitionGroup - Não estou usando classes CSS, são todos estilos JS, então isso não funcionará.
  2. ReactTransitionGroup- Esta API de nível inferior é ótima, mas requer que você use um retorno de chamada quando a animação estiver concluída, então apenas usar transições CSS não funcionará aqui. Sempre existem bibliotecas de animação, o que nos leva ao próximo ponto:
  3. GreenSock - O licenciamento é muito restritivo para uso comercial IMO.
  4. React Motion - Parece ótimo, mas TransitionMotioné extremamente confuso e complicado demais para o que preciso.
  5. Claro que posso fazer truques como o Material UI faz, em que os elementos são renderizados, mas permanecem ocultos ( left: -10000px), mas prefiro não seguir esse caminho. Eu considero isso hacky e quero que meus componentes sejam desmontados para que sejam limpos e não atrapalhem o DOM.

Eu quero algo que seja fácil de implementar. Na montagem, anime um conjunto de estilos; ao desmontar, anime o mesmo (ou outro) conjunto de estilos. Feito. Também deve ser de alto desempenho em várias plataformas.

Eu bati em uma parede de tijolos aqui. Se estiver faltando alguma coisa e houver uma maneira fácil de fazer isso, me avise.

ffxsam
fonte
Que tipo de animação estamos falando aqui?
Pranesh Ravi de
Apenas algo simples, como um fade in de opacidade CSS e umtransform: scale
ffxsam
Os pontos 1 e 2 me confundem. Que tipo de animação você está usando? Transições JS ou CSS?
Pranesh Ravi
1
Não confunda estilos / classes CSS (por exemplo .thing { color: #fff; }) com estilos JS ( const styles = { thing: { color: '#fff' } }))
ffxsam
Mas o problema é que, quando você tenta mudar o estilo usando javascript, você está na verdade substituindo o estilo de um elemento que não dará nenhuma transição.
Pranesh Ravi de

Respostas:

102

Isso é um pouco demorado, mas usei todos os eventos e métodos nativos para obter essa animação. Não ReactCSSTransitionGroup, ReactTransitionGroupe etc.

Coisas que eu usei

  • Métodos de ciclo de vida de reação
  • onTransitionEnd evento

Como isso funciona

  • Monte o elemento com base no suporte de montagem passado ( mounted) e com o estilo padrão ( opacity: 0)
  • Após a montagem ou atualização, use componentDidMount( componentWillReceivePropspara atualizações posteriores) para alterar o estilo ( opacity: 1) com um tempo limite (para torná-lo assíncrono).
  • Durante a desmontagem, passe um prop para o componente para identificar a desmontagem, mude o estilo novamente ( opacity: 0) onTransitionEnd, remova desmontar o elemento do DOM.

Continue o ciclo.

Percorra o código, você entenderá. Se precisar de algum esclarecimento, por favor deixe um comentário.

Espero que isto ajude.

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>

Pranesh Ravi
fonte
Obrigado por isso! Sobre onde você aprendeu onTransitionEnd? Não vejo isso nos documentos do React.
ffxsam
@ffxsam facebook.github.io/react/docs/events.html Está nos eventos de transição.
Pranesh Ravi
1
Como você sabia o que isso fazia, a documentação não explica nada. Outra pergunta: como você sabia que componentWillReceivePropspode devolver algo? Onde posso ler mais sobre isso?
ffxsam
1
@ffxsam onTransitionEnd é um evento JavaScript nativo. Você pode google sobre isso. facebook.github.io/react/docs/… lhe dará uma ideia sobre o componentWillReceiveProps.
Pranesh Ravi
7
BTW, acho que há um erro no seu código. Em seu Parentcomponente, você faz referência athis.transitionEnd
ffxsam
14

Usando o conhecimento adquirido com a resposta de Pranesh, eu vim com uma solução alternativa que é configurável e reutilizável:

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

Uso:

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

E finalmente, no rendermétodo de outro componente :

return <div>
  <ReactTransitionGroup>
    <Thing />
  </ReactTransitionGroup>
</div>
ffxsam
fonte
1
E como você monta / desmonta @ffxsam?
Como é componentWillLeave()e é componentWillEnter()chamado AnimatedMount?
Rokit
Não funciona para mim, aqui está minha sandbox: codesandbox.io/s/p9m5625v6m
Webwoman
11

Aqui está minha solução usando a nova API de ganchos (com TypeScript), baseada neste post , para atrasar a fase de desmontagem do componente:

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

Uso:

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

Link CodeSandbox .

deckele
fonte
1
solução elegante, seria ótimo se você tivesse adicionado alguns comentários :)
Webwoman
também por que usar a extensão do typescrypt já que funciona bem na extensão do javascript?
Webwoman de
também o console retorna "não é possível encontrar o tempo limite do NodeJS do namespace"
Webwoman
1
@Webwoman Obrigado por seus comentários. Não consigo recriar o seu problema relatado com "tempo limite do NodeJS", consulte meu link CodeSandbox abaixo da resposta. Quanto ao TypeScript, pessoalmente prefiro usá-lo em vez de JavaScript, embora ambos sejam viáveis, é claro.
deckele
9

Eu me opus a esse problema durante meu trabalho e, por mais simples que pareça, realmente não está no React. Em um cenário normal em que você renderiza algo como:

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

conforme as this.state.showalterações, os filhos são montados / desmontados imediatamente.

Uma abordagem que Animateusei foi criar um componente wrapper e usá-lo como

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

agora, como this.state.showmudanças, podemos perceber mudanças de objetos getDerivedStateFromProps(componentWillReceiveProps)e criar estágios intermediários de renderização para realizar animações.

Um ciclo de estágio pode ser assim

Começamos com o Estágio Estático quando os filhos são montados ou desmontados.

Depois de detectar as showmudanças de sinalizador, entramos no estágio de preparação, onde calculamos as propriedades necessárias como heighte widthde ReactDOM.findDOMNode.getBoundingClientRect().

Em seguida, entrando no Animate State , podemos usar a transição css para alterar a altura, largura e opacidade de 0 para os valores calculados (ou para 0 se desmontar).

No final da transição, usamos onTransitionEndapi para voltar ao Staticpalco.

Há muito mais detalhes sobre como os estágios são transferidos sem problemas, mas essa pode ser uma ideia geral :)

Se alguém estiver interessado, criei uma biblioteca React https://github.com/MingruiZhang/react-animate-mount para compartilhar minha solução. Qualquer feedback é bem-vindo :)

Mingrui Zhang
fonte
Obrigado por seus comentários, desculpe pela resposta grosseira antes. Eu adicionei mais detalhes e um diagrama à minha resposta, espero que isso possa ser mais útil para outras pessoas.
Mingrui Zhang
1
@MingruiZhang É bom ver que você aceitou os comentários de forma positiva e melhorou sua resposta. É muito revigorante ver. Bom trabalho.
Bugs
6

Acho que usar Transitiondo react-transition-groupé provavelmente a maneira mais fácil de rastrear a montagem / desmontagem. É incrivelmente flexível. Estou usando algumas classes para mostrar como é fácil de usar, mas você pode definitivamente ligar suas próprias animações JS utilizando addEndListenerprop - com o qual tive muita sorte usando GSAP também.

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

E aqui está meu código.

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);
Shalanah
fonte
1
Se você usa componentes estilizados, pode simplesmente passar a showpropriedade H1e fazer toda a lógica dentro do componente estilizado. Como ...animation: ${({ show }) => show ? entranceKeyframes : exitKeyframes} 300ms ease-out forwards;
Aleks
2

Movimento Framer

Instale o framer-motion do 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>
)
Tony95
fonte
1

Para aqueles que estão considerando o movimento de reação, animar um único componente quando ele é montado e desmontado pode ser difícil de configurar.

Existe uma biblioteca chamada react-motion-ui-pack que torna este processo muito mais fácil de começar. É um invólucro em torno do movimento de reação, o que significa que você obtém todos os benefícios da biblioteca (ou seja, você pode interromper a animação, ter várias desmontagens ao mesmo tempo).

Uso:

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 define qual deve ser o estado final do componente; deixar é o estilo aplicado quando o componente é desmontado.

Você pode descobrir que, depois de usar o pacote de interface do usuário algumas vezes, a biblioteca de reação-movimento pode não ser mais tão assustadora.

Björn Holdt
fonte
O projeto não é mais mantido (2018)
Micros
1

Isso pode ser feito facilmente usando o CSSTransitioncomponente de react-transition-group, que é igual às bibliotecas que você mencionou. O truque é que você precisa envolver o componente CSSTransition sem um mecanismo mostrar / ocultar como você normalmente faria.{show && <Child>}... Ou seja, você está ocultando a animação e ela não funcionará. Exemplo:

ParentComponent.js

import React from 'react';
import {CSSTransition} from 'react-transition-group';

function ParentComponent({show}) {
return (
  <CSSTransition classes="parentComponent-child" in={show} timeout={700}>
    <ChildComponent>
  </CSSTransition>
)}


ParentComponent.css

// animate in
.parentComponent-child-enter {
  opacity: 0;
}
.parentComponent-child-enter-active {
  opacity: 1;
  transition: opacity 700ms ease-in;
}
// animate out
.parentComponent-child-exit {
  opacity: 1;
}
.parentComponent-child-exit-active {
  opacity: 0;
  transition: opacity 700ms ease-in;
}
Lauren
fonte
0

Aqui estão meus 2 centavos: obrigado a @deckele pela solução. Minha solução é baseada na dele, é a versão do componente stateful, totalmente reutilizável.

aqui minha sandbox: https://codesandbox.io/s/302mkm1m .

aqui meu 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);
Webwoman
fonte
0

Veja como eu resolvi isso em 2019, enquanto fazia um botão giratório de carregamento. Estou usando componentes funcionais do React.

Eu tenho um componente de aplicativo pai que possui um componente Spinner filho .

O aplicativo determina se o aplicativo está carregando ou não. Quando o aplicativo está carregando, Spinner é renderizado normalmente. Quando o aplicativo não está carregando ( isLoadingé falso), o Spinner é renderizado com o 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;

O Spinner define se está oculto ou não. No início, com adereços e estado padrão, Spinner é renderizado normalmente. A Spinner-fadeInclasse o anima com o esmaecimento. Quando o Spinner recebe o adereço, shouldUnmountele renderiza com a Spinner-fadeOutclasse, animando-o com o esmaecimento.

No entanto, eu também queria que o componente desmontasse depois de desaparecer.

Neste ponto, tentei usar o onAnimationEndevento sintético React, semelhante à solução de @pranesh-ravi acima, mas não funcionou. Em vez disso, costumava setTimeoutdefinir o estado como oculto com um atraso da mesma duração da animação. O Spinner será atualizado após o atraso com isHidden === truee nada será renderizado.

A chave aqui é que o pai não desmonta o filho, ele diz ao filho quando desmontar e o filho desmonta a si mesmo depois de cuidar de seus negócios de desmontagem.

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);
    }
}
mjw
fonte
0

Eu também precisava urgentemente de animação de um único componente. Eu cansei de usar React Motion, mas estava puxando meus cabelos por um assunto tão trivial .. (coisa que eu). Depois de pesquisar no Google, encontrei este post no repositório git deles. Espero que ajude alguém ..

Referenciado de e também o crédito . Isso funciona para mim a partir de agora. Meu caso de uso foi um modal para animar e desmontar no caso de carregar e descarregar.

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

Rahul Singh
fonte
0

Eu sei que há muitas respostas aqui, mas ainda não encontrei uma que se adequasse às minhas necessidades. Eu quero:

  • Componentes funcionais
  • Uma solução que permitirá que meus componentes fade in / out facilmente quando forem montados / desmontados.

Depois de muitas horas mexendo, eu tenho uma solução que funciona, diria 90%. Eu escrevi a limitação em um bloco de comentário no código abaixo. Eu ainda adoraria uma solução melhor, mas esta é a melhor que encontrei, incluindo as outras soluções aqui.

const TIMEOUT_DURATION = 80 // Just looked like best balance of silky smooth and stop delaying me.

// Wrap this around any views and they'll fade in and out when mounting /
// unmounting.  I tried using <ReactCSSTransitionGroup> and <Transition> but I
// could not get them to work.  There is one major limitation to this approach:
// If a component that's mounted inside of <Fade> has direct prop changes,
// <Fade> will think that it's a new component and unmount/mount it.  This
// means the inner component will fade out and fade in, and things like cursor
// position in forms will be reset. The solution to this is to abstract <Fade>
// into a wrapper component.

const Fade: React.FC<{}> = ({ children }) => {
  const [ className, setClassName ] = useState('fade')
  const [ newChildren, setNewChildren ] = useState(children)

  const effectDependency = Array.isArray(children) ? children : [children]

  useEffect(() => {
    setClassName('fade')

    const timerId = setTimeout(() => {
      setClassName('fade show')
      setNewChildren(children)
    }, TIMEOUT_DURATION)

    return () => {
      clearTimeout(timerId)
    }   

  }, effectDependency)

  return <Container fluid className={className + ' p-0'}>{newChildren}</Container>
}

Se você tem um componente que deseja fade in / out, envolva-o em <Fade>Ex. <Fade><MyComponent/><Fade>.

Observe que isso é usado react-bootstrappara os nomes das classes e para <Container/>, mas ambos podem ser facilmente substituídos por CSS personalizado e um antigo regular <div>.

Aaron Sullivan
fonte
0

Se eu usar Velocityou a AnimeJSbiblioteca para animar o nó diretamente (em vez de cssou setTimeout), descobri que posso criar um hookpara fornecer o status da animação one a função onTogglepara iniciar a animação (por exemplo, slidedown, fade).

Basicamente, o que o gancho faz é ligar e desligar a animação e depois atualizá-la de onacordo. Portanto, podemos obter o status da animação com precisão. Sem fazer isso, responderia em um ad-hoc duration.

/**
 * A hook to provide animation status.
 * @class useAnimate
 * @param {object} _                props
 * @param {async} _.animate         Promise to perform animation
 * @param {object} _.node           Dom node to animate
 * @param {bool} _.disabled         Disable animation
 * @returns {useAnimateObject}      Animate status object
 * @example
 *   const { on, onToggle } = useAnimate({
 *    animate: async () => { },
 *    node: node
 *  })
 */

import { useState, useCallback } from 'react'

const useAnimate = ({
  animate, node, disabled,
}) => {
  const [on, setOn] = useState(false)

  const onToggle = useCallback(v => {
    if (disabled) return
    if (v) setOn(true)
    animate({ node, on: v }).finally(() => {
      if (!v) setOn(false)
    })
  }, [animate, node, disabled, effect])

  return [on, onToggle]
}

export default useAnimate

O uso é o seguinte,

  const ref = useRef()
  const [on, onToggle] = useAnimate({
    animate: animateFunc,
    node: ref.current,
    disabled
  })
  const onClick = () => { onToggle(!on) }

  return (
      <div ref={ref}>
          {on && <YOUROWNCOMPONENT onClick={onClick} /> }
      </div>
  )

e a implementação animada pode ser,

import anime from 'animejs'

const animateFunc = (params) => {
  const { node, on } = params
  const height = on ? 233 : 0
  return new Promise(resolve => {
    anime({
      targets: node,
      height,
      complete: () => { resolve() }
    }).play()
  })
}
Windmaomao
fonte