Reaguj - animuj montowanie i odmontowywanie pojedynczego komponentu

103

Coś tak prostego powinno być łatwe do osiągnięcia, ale wyrywam sobie włosy z tego, jak skomplikowane jest to.

Chcę tylko animować montaż i odmontowywanie komponentu Reacta, to wszystko. Oto, czego próbowałem do tej pory i dlaczego każde rozwiązanie nie działa:

  1. ReactCSSTransitionGroup - W ogóle nie używam klas CSS, to wszystkie style JS, więc to nie zadziała.
  2. ReactTransitionGroup- Ten interfejs API niższego poziomu jest świetny, ale wymaga użycia wywołania zwrotnego po zakończeniu animacji, więc samo użycie przejść CSS nie będzie tutaj działać. Zawsze istnieją biblioteki animacji, co prowadzi do następnego punktu:
  3. GreenSock - licencja jest zbyt restrykcyjna do użytku biznesowego IMO.
  4. React Motion - Wydaje się świetne, ale TransitionMotionjest bardzo zagmatwane i zbyt skomplikowane w stosunku do tego, czego potrzebuję.
  5. Oczywiście mogę po prostu robić sztuczki, takie jak Material UI, w którym elementy są renderowane, ale pozostają ukryte ( left: -10000px), ale wolałbym nie iść tą drogą. Uważam to za zepsute i chcę, aby moje komponenty odmontowały się, aby wyczyściły i nie zaśmiecały DOM.

Chcę czegoś, co jest łatwe do wdrożenia. Na wierzchu animuj zestaw stylów; po odmontowaniu animuj ten sam (lub inny) zestaw stylów. Gotowe. Musi być również wysoka wydajność na wielu platformach.

Trafiłem tutaj w ceglaną ścianę. Jeśli czegoś mi brakuje i jest na to łatwy sposób, daj mi znać.

ffxsam
źródło
O jakiej animacji mówimy?
Pranesh Ravi
Po prostu coś prostego, jak zniknięcie przezroczystości CSS itransform: scale
ffxsam
Punkt 1 i 2 dezorientują mnie. Jakiego rodzaju animacji używasz? Przejścia JS czy przejścia CSS?
Pranesh Ravi
1
Nie myl stylów / klas CSS (np. .thing { color: #fff; }) const styles = { thing: { color: '#fff' } }
Ze
Ale problem polega na tym, że kiedy próbujesz zmienić styl za pomocą javascript, w rzeczywistości zastępujesz styl elementu, który nie daje żadnego przejścia.
Pranesh Ravi

Odpowiedzi:

104

To trochę za długie, ale wykorzystałem wszystkie natywne zdarzenia i metody, aby osiągnąć tę animację. Nie ReactCSSTransitionGroup, ReactTransitionGroupitd.

Rzeczy, których użyłem

  • Reaguj na metody cyklu życia
  • onTransitionEnd zdarzenie

Jak to działa

  • Zamontuj element na podstawie właściwości mount przekazanej ( mounted) iz domyślnym stylem ( opacity: 0)
  • Po zamontowaniu lub aktualizacji użyj componentDidMount( componentWillReceivePropsdla dalszych aktualizacji), aby zmienić styl ( opacity: 1) z limitem czasu (aby uczynić go asynchronicznym).
  • Podczas odmontowywania, przekaż prop do komponentu, aby zidentyfikować odmontowanie, zmień styl ponownie ( opacity: 0) onTransitionEnd,, usuń odmontowanie elementu z DOM.

Kontynuuj cykl.

Przejrzyj kod, zrozumiesz. Jeśli potrzebne są jakieś wyjaśnienia, zostaw komentarz.

Mam nadzieję że to pomoże.

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
źródło
Dzięki za to! Gdzie się dowiedziałeś onTransitionEnd? Nie widzę tego w dokumentach React.
ffxsam
@ffxsam facebook.github.io/react/docs/events.html To jest w ramach wydarzeń przejścia.
Pranesh Ravi
1
Skąd jednak wiedziałeś, co zrobił, dokumentacja niczego nie wyjaśnia. Kolejne pytanie: skąd wiedziałeś, że componentWillReceivePropsmożesz coś zwrócić? Gdzie mogę przeczytać więcej na ten temat?
ffxsam
1
@ffxsam onTransitionEnd to natywne zdarzenie JavaScript. Możesz o tym wygooglować. facebook.github.io/react/docs/… da ci wyobrażenie o komponencie componentWillReceiveProps.
Pranesh Ravi
7
Swoją drogą, myślę, że w Twoim kodzie jest błąd. W swoim Parentkomponencie odwołujesz się dothis.transitionEnd
ffxsam,
15

Korzystając z wiedzy zdobytej dzięki odpowiedzi Pranesh, wymyśliłem alternatywne rozwiązanie, które można konfigurować i używać wielokrotnie:

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

Stosowanie:

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

I wreszcie w rendermetodzie innego komponentu :

return <div>
  <ReactTransitionGroup>
    <Thing />
  </ReactTransitionGroup>
</div>
ffxsam
źródło
1
Jak zamontować / odmontować @ffxsam?
Jak się masz componentWillLeave()i jak componentWillEnter()zostać wezwanym AnimatedMount?
Rokit
U mnie nie działa, tutaj moja piaskownica : codeandbox.io/s/p9m5625v6m
Webwoman
15

Oto moje rozwiązanie wykorzystujące nowe API hooków (z TypeScript), oparte na tym poście , do opóźnienia fazy odmontowania komponentu:

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

Stosowanie:

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 do CodeSandbox .

deckele
źródło
2
eleganckie rozwiązanie, byłoby super, gdybyś dodał kilka komentarzy :)
Webwoman
po co też używać rozszerzenia typescrypt, skoro dobrze działa w rozszerzeniu javascript?
Webwoman
również konsola zwraca „nie można znaleźć przestrzeni nazw NodeJS timeout”
Webwoman
1
@Webwoman Dzięki za komentarze. Nie mogę odtworzyć zgłoszonego problemu z „przekroczeniem limitu czasu NodeJS”, zobacz mój link CodeSandbox pod odpowiedzią. Jeśli chodzi o TypeScript, osobiście wolę używać go zamiast JavaScript, chociaż oba są oczywiście wykonalne.
deckele
10

Rozwiązałem ten problem podczas mojej pracy i, jak się wydawało, proste, tak naprawdę nie jest w React. W normalnym scenariuszu, w którym renderujesz coś takiego:

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

wraz ze this.state.showzmianami elementy podrzędne są montowane / odmontowywane od razu.

Jedną z metod, które wybrałem, jest utworzenie komponentu opakowania Animatei używanie go w podobny sposób

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

teraz jako this.state.showzmiany możemy postrzegać zmiany rekwizytów getDerivedStateFromProps(componentWillReceiveProps)i tworzyć pośrednie etapy renderowania do wykonywania animacji.

Tak mógłby wyglądać cykl etapowy

Zaczynamy od Static Stage, gdy dzieci są zamontowane lub odmontowane.

Po wykryciu showzmiany flagi wchodzimy do etapu przygotowawczego, w którym obliczamy niezbędne właściwości, takie jak heighti widthod ReactDOM.findDOMNode.getBoundingClientRect().

Następnie wchodząc do programu Animate State , możemy użyć przejścia css do zmiany wysokości, szerokości i krycia z 0 na obliczone wartości (lub na 0 w przypadku odmontowania).

Pod koniec przejścia używamy onTransitionEndinterfejsu API, aby wrócić do Staticetapu.

Jest o wiele więcej szczegółów na temat płynnego przenoszenia etapów, ale może to być ogólny pomysł :)

Jeśli ktoś jest zainteresowany, utworzyłem bibliotekę React https://github.com/MingruiZhang/react-animate-mount, aby udostępnić moje rozwiązanie. Wszelkie uwagi mile widziane :)

Mingrui Zhang
źródło
Dziękuję za opinię, przepraszam za wczesną odpowiedź. Dodałem więcej szczegółów i diagram do mojej odpowiedzi, mam nadzieję, że może to być bardziej pomocne dla innych.
Mingrui Zhang
2
@MingruiZhang Dobrze jest zobaczyć, że Twoje komentarze zostały przyjęte pozytywnie i poprawiłaś swoją odpowiedź. To bardzo odświeżające. Dobra robota.
Błędy,
6

Myślę, że używanie Transitionfrom react-transition-groupjest prawdopodobnie najłatwiejszym sposobem śledzenia montażu / demontażu. Jest niesamowicie elastyczny. Używam niektórych klas, aby pokazać, jak łatwe jest w użyciu, ale zdecydowanie możesz podłączyć własne animacje JS za pomocą addEndListenerrekwizytu - z czym miałem również dużo szczęścia, używając GSAP.

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

A oto mój kod.

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
źródło
1
Jeśli używasz komponentów ze stylem, możesz po prostu przekazać showprop do H1i wykonać całą logikę wewnątrz komponentu ze stylem. Jak ...animation: ${({ show }) => show ? entranceKeyframes : exitKeyframes} 300ms ease-out forwards;
Aleks
3

Ruch kadrowania

Zainstaluj framer-motion z 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
źródło
1

Dla tych, którzy rozważają ruch reagujący, animowanie pojedynczego komponentu podczas montażu i demontażu może być przytłaczające.

Istnieje biblioteka o nazwie react-motion-ui-pack, która znacznie ułatwia rozpoczęcie tego procesu. Jest to wrapper wokół react-motion, co oznacza, że ​​masz wszystkie korzyści z biblioteki (tj. Możesz przerwać animację, mieć wiele odmontowań w tym samym czasie).

Stosowanie:

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 określa, jaki powinien być stan końcowy komponentu; opuszczenie to styl stosowany w przypadku odmontowania komponentu.

Może się okazać, że po kilkukrotnym użyciu pakietu interfejsu użytkownika biblioteka reagowania na ruch może nie być już tak zniechęcająca.

Björn Holdt
źródło
Projekt nie jest już obsługiwany (2018)
Micros
1

Można to łatwo zrobić za pomocą CSSTransitionkomponentu z programu react-transition-group, który jest podobny do wspomnianych bibliotek. Sztuczka polega na tym, że musisz owinąć komponent CSSTransition bez mechanizmu show / hide, tak jak zwykle .ie {show && <Child>}...W przeciwnym razie ukrywasz animację i nie zadziała. Przykład:

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
źródło
0

Tutaj moje 2 centy: dzięki @deckele za jego rozwiązanie. Moje rozwiązanie jest oparte na jego, jest to wersja składnika Stateful, w pełni wielokrotnego użytku.

tutaj moja piaskownica: https://codesandbox.io/s/302mkm1m .

tutaj mój 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
źródło
0

Oto, jak rozwiązałem to w 2019 roku, robiąc spinner ładujący. Używam funkcjonalnych komponentów React.

Mam nadrzędny składnik aplikacji , który ma podrzędny składnik Spinner .

Aplikacja ma stan określający, czy aplikacja się ładuje, czy nie. Podczas ładowania aplikacji Spinner jest renderowany normalnie. Gdy aplikacja się nie ładuje ( isLoadingjest fałszywa) Spinner jest renderowany z właściwością 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 ma stan określający, czy jest ukryty, czy nie. Na początku, z domyślnymi właściwościami i stanem, Spinner jest renderowany normalnie. Spinner-fadeInKlasa ożywia to blaknięcie. Kiedy Spinner odbiera rekwizyt shouldUnmountto świadczy o Spinner-fadeOutklasie zamiast animacji to wygaszanie.

Jednak chciałem również, aby komponent odmontował się po wygaszeniu.

W tym momencie próbowałem użyć onAnimationEndzdarzenia syntetycznego React, podobnego do rozwiązania @ pranesh-ravi powyżej, ale nie zadziałało. Zamiast tego setTimeoutustawiłem stan na ukryty z opóźnieniem o takiej samej długości jak animacja. Spinner zaktualizuje się po opóźnieniu za pomocą isHidden === truei nic nie zostanie renderowane.

Kluczem tutaj jest to, że rodzic nie odmontowuje dziecka, mówi mu, kiedy odmontować, a dziecko odmontowuje się po tym, jak zajmie się odmontowaniem.

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
źródło
0

Pilnie potrzebowałem też animacji jednoskładnikowej. Zmęczyłem się używaniem React Motion, ale szarpałem za włosy z tak trywialnego problemu… (rzecz). Po pewnym googlowaniu natknąłem się na ten post na ich repozytorium git. Mam nadzieję, że to komuś pomoże ...

Odniesienie od, a także kredyt . To działa dla mnie od teraz. Mój przypadek użycia był modalny do animacji i odmontowania w przypadku ładowania i usuwania.

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
źródło
0

Wiem, że jest tu wiele odpowiedzi, ale nadal nie znalazłem takiej, która odpowiadałaby moim potrzebom. Chcę:

  • Komponenty funkcjonalne
  • Rozwiązanie, które pozwoli moim komponentom łatwo zanikać / znikać, gdy są montowane / odmontowywane.

Po wielu godzinach majstrowania mam rozwiązanie, które działa, powiedziałbym, że 90%. Zapisałem ograniczenie w bloku komentarza w poniższym kodzie. Nadal chciałbym mieć lepsze rozwiązanie, ale jest to najlepsze, jakie znalazłem, w tym inne rozwiązania tutaj.

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

Jeśli masz komponent, który chcesz zanikać / zanikać, zawiń go w <Fade>Ex. <Fade><MyComponent/><Fade>.

Zauważ, że używa react-bootstrapto nazw klas i for <Container/>, ale oba można łatwo zastąpić niestandardowym CSS i zwykłym starym <div>.

Aaron Sullivan
źródło
0

Jeśli używam biblioteki Velocitylub AnimeJSdo bezpośredniej animacji węzła (zamiast csslub setTimeout), dowiedziałem się, że mogę zaprojektować hookstan animacji oni funkcję onToggleuruchamiania animacji (np. Slidedown, fade).

Zasadniczo to, co robi hak, to włączanie i wyłączanie animacji, a następnie odpowiednia aktualizacja on. Dzięki temu możemy dokładnie uzyskać stan animacji. Bez tego odpowiedziałbym na bieżąco 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

Użycie jest następujące,

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

a animowana implementacja może być,

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
źródło
0

Zawsze możesz użyć metod cyklu życia React, ale reakcja-przejście-grupa jest zdecydowanie najwygodniejszą biblioteką dla animacji, z którymi się spotkałem, niezależnie od tego, czy używasz, styled-componentsczy zwykłego css. Jest to szczególnie przydatne, gdy chcesz śledzić montaż i odmontowywanie komponentu i odpowiednio renderować animacje. Używaj Transitionze styled-components i CSSTransitiongdy używasz zwykłych nazw klas CSS.

webmessi
źródło