Реагировать - анимация монтирования и размонтирования одного компонента

Что-то такое простое должно быть легко выполнено, и все же я дергаюсь за то, насколько это сложно.

Все, что я хочу сделать, это оживить монтирование и демонтаж компонента React, вот и все. Вот что я пробовал до сих пор и почему каждое решение не будет работать:

  1. ReactCSSTransitionGroup - Я вообще не использую CSS-классы, это все стили JS, так что это не сработает.
  2. ReactTransitionGroup - Этот API нижнего уровня хорош, но он требует использования обратного вызова, когда анимация завершена, поэтому простое использование переходов CSS здесь не будет работать. Всегда есть библиотеки анимации, что приводит к следующему пункту:
  3. GreenSock - Лицензирование слишком ограничено для бизнес-использования IMO.
  4. React Motion - это выглядит здорово, но TransitionMotion очень запутанный и слишком сложный для того, что мне нужно.
  5. Конечно, я могу просто делать хитрости, как это делает Material UI, где элементы визуализируются, но остаются скрытыми (left: -10000px) но я бы предпочел не идти по этому пути. Я считаю это хакерским, и я хочу, чтобы мои компоненты были размонтированы, чтобы они очищались и не загромождали DOM.

Я хочу что-то, что легко реализовать. На горе анимируйте набор стилей; при размонтировании анимируйте тот же (или другой) набор стилей. Готово. Это также должно быть высокая производительность на нескольких платформах.

Я ударил кирпичную стену здесь. Если я что-то упустил и есть простой способ сделать это, дайте мне знать.

24 ответа

Решение

Это немного длинно, но я использовал все нативные события и методы для достижения этой анимации. нет ReactCSSTransitionGroup, ReactTransitionGroup и так далее.

Вещи, которые я использовал

  • Реагировать на методы жизненного цикла
  • onTransitionEnd событие

Как это работает

  • Смонтируйте элемент, основываясь на переданном креплении (mounted) и со стилем по умолчанию (opacity: 0)
  • После монтирования или обновления используйте componentDidMount (componentWillReceiveProps для дальнейших обновлений) поменять стиль (opacity: 1) с таймаутом (чтобы сделать его асинхронным).
  • Во время размонтирования передайте опору компоненту, чтобы определить размонтирование, снова измените стиль (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>

Вот мое решение, использующее новый API хуков (с TypeScript), основанный на этом посте, для задержки фазы размонтирования компонента:

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

Использование:

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.

Я боролся с этой проблемой во время своей работы, и, как мне показалось, ее просто нет в React. В обычном сценарии, где вы рендерите что-то вроде:

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

как this.state.show изменения дети монтируются / размонтируются сразу.

Один из подходов, который я выбрал, - это создание компонента-оболочки Animate и использовать его как

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

сейчас как this.state.show изменения, мы можем воспринимать изменения опоры с getDerivedStateFromProps(componentWillReceiveProps) и создавать промежуточные этапы рендеринга для выполнения анимации.

Сценический цикл может выглядеть так

Мы начинаем со статической сцены, когда дети монтируются или демонтируются.

Как только мы обнаружим show флаг меняется, мы входим в этап подготовки, где мы вычисляем необходимые свойства, такие как height а также width от ReactDOM.findDOMNode.getBoundingClientRect(),

Затем, войдя в Animate State, мы можем использовать css transition, чтобы изменить высоту, ширину и непрозрачность с 0 на расчетные значения (или на 0, если отключить).

В конце перехода мы используем onTransitionEnd API, чтобы изменить обратно Static этап.

Есть намного больше деталей о том, как этапы переходят гладко, но это может быть общей идеей:)

Если кому-то интересно, я создал библиотеку React https://github.com/MingruiZhang/react-animate-mount чтобы поделиться своим решением. Любые отзывы приветствуются:)

Используя знания, полученные из ответа Пранеша, я придумал альтернативное решение, которое можно настраивать и использовать повторно:

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>

Framer motion

Установите framer-motion из 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>
)

Я думаю, используя Transition от react-transition-group это, вероятно, самый простой способ отслеживать монтаж / демонтаж. Это невероятно гибкий. Я использую некоторые классы, чтобы показать, как это легко использовать, но вы, безусловно, можете подключить свои собственные JS-анимации, используя addEndListener prop - с которым мне очень повезло, используя 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);

Для тех, кто рассматривает реагирующее движение, анимация отдельного компонента при его монтировании и размонтировании может быть изнурительной.

Существует библиотека под названием response-motion-ui-pack, с которой намного легче начать этот процесс. Это обёртка вокруг response-motion, что означает, что вы получаете все преимущества библиотеки (то есть вы можете прерывать анимацию, когда происходит несколько размонтирований одновременно).

Использование:

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 определяет, каким должно быть конечное состояние компонента; оставить это стиль, который применяется, когда компонент не подключен.

Возможно, вы обнаружите, что после того, как вы несколько раз воспользовались пакетом пользовательского интерфейса, библиотека реагирования на движение может перестать быть устрашающей.

Вы можете сделать это с помощью React Transition Group. Он дает вам классы CSS, так что вы можете писать свой код анимации в этих классах CSS.

Следуйте этому простому примеру

      import {CSSTransition } from 'react-transition-group';//This should be imported
import './AnimatedText.css';

const AnimatedText = () => {
    const [showText, setShowText] = useState(false); //By default text will be not shown

    //Handler to switch states
    const switchHandler = () =>{
        setShowText(!showText);
    };

    return (
        //in : pass your state here, it will used by library to toggle. It should be boolean
        //timeout: your amination total time(it should be same as mentioned in css)
        //classNames: give class name of your choice, library will prefix it with it's animation classes
        //unmountOnExit: Component will be unmounted when your state changes to false
        <CSSTransition in={showText} timeout={500} classNames='fade' unmountOnExit={true}>
            <h1>Animated Text</h1>
        </CSSTransition>  
        <button onClick={switchHandler}>Show Text</button>                  
    );
};

export default AnimatedText;

Теперь давайте напишем анимацию в файле CSS (AnimatedText.css), запомните свойство classNames (в данном случае затухание).

      //fade class should be prefixed

/*****Fade In effect when component is mounted*****/
//This is when your animation starts
fade-enter {
  opacity: 0;
}

//When your animation is active
.fade-enter.fade-enter-active {
  opacity: 1;
  transition: all 500ms ease-in;
}
/*****Fade In effect when component is mounted*****/


/*****Fade Out effect when component is unmounted*****/
.fade-exit {
  opacity: 1;
}
.fade-exit-active {
  opacity: 0;
  transition: all 500ms ease-out;
}

/*****Fade Out effect when component is unmounted*****/

Также есть класс появления, который можно использовать, когда ваш компонент загружается в первый раз. Проверьте документацию для более подробной информации

Анимация входных и выходных переходов намного легче с реагирующим движением.

пример с кодами

Я ищу решение без каких-либо дополнительных зависимостей, и вот крючок, который у меня отлично работает:

      import { useState, useEffect, CSSProperties } from "react";

export function useAnimatedUnmount(isMounted: boolean, delayTime: number) {
  const mountedStyle: CSSProperties = {
    opacity: 1,
    transform: "scale(1)",
    transition: `all ${delayTime}ms ease-out`,
  };
  const unmountedStyle: CSSProperties = {
    opacity: 0,
    transform: "scale(0)",
    transition: `all ${delayTime}ms ease-in`,
  };

  const [style, setStyle] = useState(isMounted ? mountedStyle : unmountedStyle);
  const [showComponent, setShowComponent] = useState(isMounted);

  useEffect(() => {
    let timeoutId: NodeJS.Timeout;
    if (isMounted) {
      setShowComponent(true);
      timeoutId = setTimeout(() => setStyle(mountedStyle), 10);
    } else {
      timeoutId = setTimeout(() => setShowComponent(false), delayTime);
      setStyle(unmountedStyle);
    }
    return () => clearTimeout(timeoutId);
  }, [isMounted, delayTime]);
  return { showComponent, style };
}

и вот реализация:

      export default function App() {
  const [isDialogOpen, setIsDialogOpen] = useState(false);
  const { showComponent, style } = useAnimatedUnmount(isDialogOpen, 200);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <button onClick={() => setIsDialogOpen(!isDialogOpen)}>
        {isDialogOpen ? "CLose" : "Open"}
      </button>
      {showComponent && (
        <h2 style={style}>Start editing to see some magic happen!</h2>
      )}
    </div>
  );
}

ссылка на коды и ящик - https://codesandbox.io/s/lucid-tree-sij1wd?file=/src/App.tsx

Это легко сделать с помощью CSSTransition компонент из react-transition-group, который похож на упомянутые вами библиотеки. Хитрость заключается в том что вам нужно обернуть компонент CSSTransition без механизма показать / скрыть, как вы обычно бы.ie{show && <Child>}...В противном случае вы скрываете анимацию, и она не будет работать. Пример:

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

Для этого вы можете использовать React SyntheticEvent.

Это можно сделать с помощью таких событий, как onAnimationEnd или onTransitionEnd.

React Docs: https://reactjs.org/docs/events.html

Пример кода: https://dev.to/michalczaplinski/super-easy-react-mount-unmount-animations-with-hooks-4foj

Что если в onMount вы добавите еще одно className, в котором есть переход, а в onUnMount вы удалите это имя класса?

Я создал WrapperComponent общего назначения под названием MountAnimationтак что вы можете анимировать элементы без необходимости писать одно и то же снова и снова. Он использует CSSTransitions под капотом, поэтому вам нужно установить его.

  1. Установить зависимости
      npm install react-transition-group
  1. Создайте компонент в одной из ваших папок
      import { CSSTransition } from "react-transition-group"

export const MountAnimation = ({
  children,
  timeout = 300, // MATCH YOUR DEFAULT ANIMATION DURATION
  isVisible = false,
  unmountOnExit = true,
  classNames = "transition-translate-y", // ADD YOUR DEFAULT ANIMATION
  ...restProps
}) => {
  return (
    <CSSTransition
      in={isVisible}
      timeout={timeout}
      classNames={classNames}
      unmountOnExit={unmountOnExit}
      {...restProps}
    >
      <div>{children}</div>
    </CSSTransition>
  )
}
  1. Просто используйте его так:
      import { MountAnimation } from '../../path/to/component'

...

const [isElementVisible, setIsElementVisible] = useState(false)

return (
    <MountAnimation isVisible={isElementVisible}>
       // your content here
    </MountAnimation>

)
  1. (Проявите творческий подход) Вам нужно объявить свою анимацию в файле CSS. Убедитесь, что вы объявили это в глобально доступном файле CSS, если вы разделяете код. В этом примере я использую следующую анимацию:
      .transition-translate-y-enter {
  opacity: 0;
  transform: translateY(-5px);
}
.transition-translate-y-enter-active {
  opacity: 1;
  transform: translateY(0px);
  transition: opacity 300ms ease-in-out, transform 300ms ease-in-out;
}
.transition-translate-y-exit {
  opacity: 1;
  transform: translateY(0px);
}
.transition-translate-y-exit-active {
  opacity: 0;
  transform: translateY(-5px);
  transition: opacity 300ms ease-in-out, transform 300ms ease-in-out;
}

Вот живой пример этой реализации:

https://codesandbox.io/s/vibrant-elion-ngfzr?file=/src/App.js

переписал /questions/5512871/reagirovat-animatsiya-montirovaniya-i-razmontirovaniya-odnogo-komponenta/5512881#5512881

пожалуйста, если вы нажмете лайк на этот комментарий, лайкните и родителя, спасибо

очистить js

      import {useState, useEffect} from 'react';

const useDelayUnmount = (isMounted, msDelay = 500)=>{
    const [shouldRender, setShouldRender] = useState(false);

    useEffect(()=>{
        let timeoutId;

        if(isMounted && !shouldRender){
            setShouldRender(true);
        }else if(!isMounted && shouldRender){
            timeoutId = setTimeout(()=>setShouldRender(false), msDelay);
        }

        return ()=>clearTimeout(timeoutId);
    }, [isMounted, msDelay, shouldRender]);

    return shouldRender;
};

export default useDelayUnmount;

Приведенный ниже код несколько похож на другие ответы, но я хотел попытаться масштабировать решение.

  • чтобы легко добавлять различные типы переходов, например, затухание, скольжение и т. д.
  • использовать одно и то же решение независимо от условного рендеринга или просто рендеринга без каких-либо условий.

В приведенном ниже коде для реализации требуются только эти два элемента кода.

  1. AnimatableComponent- Компонент React, который внутренне использует .

    Примечание: можно также использоватьTransitionComponentотдельно тоже иметь переходы по компоненту, которые не рендерятся условно.

  2. И набор классов CSS.

  3. Остальное — как их использовать.

Примечания:

  • Вот ссылка на хороший пост, на который ссылались, чтобы написать этот код .
  • Также упоминается эта песочница кода для условия условного рендеринга, когда обаshowи переменные использовались как в((show || mount) && <Your Component.../>
  • Раньше я использовал только как вmount && <Your Component.../>. Я не понимаю причину, но, похоже, это как-то связано с перекомпоновкой DOM и тем, как переходы CSS нуждаются в перекомпоновке для правильной работы, когда они применяются к условно визуализируемому элементу, например, к элементу, добавленному с помощью appendChildили display: noneв JS или &&в Реакт JSX.
  • Вот еще один codeandbox из одного из ответов , в котором используется только одно условие, но в этом используется анимация CSS. У CSS-анимации, похоже, нет этой проблемы, но у CSS-переходов есть.
  • В заключение кажется, что если вы используете
    • CSS Transitions, затем используйте логическое ИЛИ обеих переменных, как в(show || mount) && <Your Component... />.
    • CSS-анимации, а затем простоmountкак вmount && <Your Component... />.

Я нашел самое простое решение, которое работает для меня. Вы можете сделать анимацию, используя только css.

Если мы используем анимацию, она обычно зациклена. Но вы можете добавить свойство, которое позволит анимации запускаться только 1 раз.

А при монтировании компонента получается, что мы запустим анимацию, которая сработает 1 раз.

(я использую попутный ветер, но принцип тот же)

Как это выглядит в компоненте (например, простой класс):

      ...
return (
    <>
      <div
        id="scrim"
        onClick={handleClose}
        className="fixed inset-0 block flex h-full w-full items-end justify-center  overflow-y-hidden bg-gray-800/50"
      >
        <div className="mt-[56px] flex animate-sheet   flex-col items-center justify-center">
          <div className=" z-10 flex w-full min-w-[220px] max-w-[640px]  flex-col overflow-y-auto  rounded-t-[16px] bg-blue-50 px-[12px] pb-[28px] scrollbar-hide dark:bg-gray-900">
...

Теперь измените конфигурацию вашего файла попутного ветра и добавьте дополнительную анимацию.

      ...
animation: {
  sheet: "sheet 0.5s cubic-bezier(.9,0,.2,1) 1",  here you can change "1" to "infinity" and get looped animation or vice versa
},...

И укажите ключевые кадры:

      ...
keyframes: {
  sheet: {
    from: {
       transform: "translateY(100%)",
      },
    to: {
       transform: "translateY(0)",
      },
},
...

Недостатком этого метода является отсутствие эффекта при размонтировании компонента. Но в моем случае, я думаю, что с точки зрения количества кода, его простоты и достигаемого эффекта, это отличный компромисс, чем перетаскивание новых зависимостей или добавление кастомных хуков.

Если я использую Velocity или AnimeJS библиотека для непосредственной анимации узла (вместо css или setTimeout), то я узнал, что могу создать hook для предоставления статуса анимации on и функция onToggle для запуска анимации (например, скольжение вниз, исчезновение).

В основном ловушка включает и выключает анимацию, а затем обновляетonсоответственно. Таким образом, мы можем точно получить статус анимации. Без этого ответил бы на специальном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

Использование следующее,

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

и анимированная реализация может быть,

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

Я также остро нуждался в однокомпонентной анимации. Я устал использовать React Motion, но я тянул за волосы из-за такой тривиальной проблемы... (я вещь). После некоторого поиска в Google я наткнулся на этот пост в их репозитории git. Надеюсь, это кому-то поможет..

Ссылка From & также кредит. На данный момент это работает для меня. Мой вариант использования был модальным для анимации и размонтирования в случае загрузки и выгрузки.

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

Вот мои 2cents: спасибо @deckele за его решение. Мое решение основано на его, это версия компонента состояния, полностью пригодная для повторного использования.

вот моя песочница: https://codesandbox.io/s/302mkm1m.

вот мой 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);

Вы всегда можете использовать методы жизненного цикла React, но react-transition-group - безусловно, самая удобная библиотека для анимаций, с которой я сталкивался, независимо от того, используете ли вы styled-componentsили простой css. Это особенно полезно, когда вы хотите отслеживать монтирование и размонтирование вашего компонента и соответственно визуализировать анимацию. Использовать Transition со стилизованными компонентами и CSSTransition когда вы используете простые имена классов css.

Вот как я решил это в 2019 году, пока делал загрузочный счетчик. Я использую функциональные компоненты React.

У меня есть родительский компонент приложения, который имеет дочерний компонент Spinner.

В приложении указано, загружается приложение или нет. Когда приложение загружается, Spinner отображается нормально. Когда приложение не загружается (isLoading неверно) Spinner отображается с опорой 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 есть состояние, скрыто оно или нет. В начале, с реквизитами и состоянием по умолчанию, Spinner отображается нормально. Spinner-fadeIn класс оживляет его исчезновение. Когда Spinner получает опору shouldUnmount это делает с Spinner-fadeOut класс вместо того, чтобы оживить его, исчезая.

Однако я также хотел отключить компонент после исчезновения.

В этот момент я попытался использовать onAnimationEnd Реагируйте на синтетическое событие, аналогичное описанному выше решению @ pranesh-ravi, но оно не сработало. Вместо этого я использовал setTimeout установить состояние скрытым с задержкой той же длины, что и анимация. Spinner будет обновляться после задержки с isHidden === trueи ничего не будет оказано.

Ключевым моментом здесь является то, что родитель не размонтирует дочерний элемент, он сообщает ему, когда размонтировать, и дочерний элемент размонтируется после того, как он позаботится о своем размонтировании.

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

Я знаю, что здесь есть много ответов, но я все еще не нашел того, который подходит для моих нужд. Я хочу:

  • Функциональные компоненты
  • Решение, которое позволит моим компонентам легко исчезать / появляться при установке / отключении.

После многих часов возни у меня есть решение, которое работает, я бы сказал, на 90%. Я написал ограничение в блоке комментариев в приведенном ниже коде. Я все еще хотел бы лучшее решение, но это лучшее, что я нашел, включая другие решения здесь.

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

Если у вас есть компонент, который вы хотите усилить / затухать, оберните его <Fade> Ex. <Fade><MyComponent/><Fade>.

Обратите внимание, что здесь используется react-bootstrap для имен классов и для <Container/>, но оба могут быть легко заменены пользовательским CSS и обычным старым <div>.

Если вы ищете простой пример хуков:

      import React, { useEffect, useReducer } from "react";
import ReactDOM from "react-dom";

const ANIMATION_TIME = 2 * 1000;

function Component() {
  const [isMounted, toggleMounted] = useReducer((p) => !p, true);
  const [isAnimateAnmount, toggleAnimateUnmount] = useReducer((p) => !p, false);
  const [isVisible, toggleVisible] = useReducer((p) => (p ? 0 : 1), 0);

  useEffect(() => {
    if (isAnimateAnmount) {
      toggleVisible();
      toggleAnimateUnmount();
      setTimeout(() => {
        toggleMounted();
      }, ANIMATION_TIME);
    }
  }, [isAnimateAnmount]);

  useEffect(() => {
    toggleVisible();
  }, [isMounted]);

  return (
    <>
      <button onClick={toggleAnimateUnmount}>toggle</button>
      <div>{isMounted ? "Mounted" : "Unmounted"}</div>
      {isMounted && (
        <div
          style={{
            fontSize: 60,
            opacity: isVisible,
            transition: "all 2s ease"
          }}
        >
          Example
        </div>
      )}
    </>
  );
}

Редактировать анимацию с размонтированием

Другие вопросы по тегам