Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

  • Смонтируйте элемент, основываясь на переданной (mounted) опоре 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>

Ответ 2

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

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>

Ответ 3

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

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

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

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

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

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

A stage cycle might look like this

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

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

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

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

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

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

Ответ 4

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

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

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

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 ссылка.

Ответ 5

Я думаю, что использование 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);

Ответ 6

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

Ответ 7

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

Там есть библиотека под названием react-motion-ui-pack, из-за чего этот процесс намного проще начать. Это обертка вокруг реактивного движения, что означает, что вы получаете все преимущества от библиотеки (т.е. Вы можете прерывать анимацию, одновременно иметь несколько размонтировок).

Применение:

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>

Ввод определяет, какое должно быть конечное состояние компонента; leave - стиль, который применяется, когда компонент размонтирован.

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

Ответ 9

Вот мои 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);

Ответ 10

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

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

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

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

В этот момент я попытался использовать синтетическое событие onAnimationEnd React, аналогичное приведенному выше решению @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);
    }
}

Ответ 11

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

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

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

Ответ 12

Это можно легко сделать с помощью компонента CSSTransition из react-transition-group, который похож на библиотеки, которые вы упомянули. Хитрость в том, что вам нужно обернуть компонент CSSTransition без механизма показа/скрытия, как вы это обычно делаете.т.е. {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;
}