react library gsap animations animation reactjs css-animations greensock react-motion

animation - library - Reaccionar: animar el montaje y desmontar un solo componente



react animation library (11)

Animar las transiciones de entrada y salida es mucho más fácil con react-move .

ejemplo en codesandbox

Algo así de simple debería lograrse fácilmente, sin embargo, me estoy sacando el pelo por lo complicado que es.

Todo lo que quiero hacer es animar el montaje y desmontaje de un componente React, eso es todo. Esto es lo que he probado hasta ahora y por qué cada solución no funcionará:

  1. ReactCSSTransitionGroup : no estoy usando clases CSS, son todos estilos JS, por lo que esto no funcionará.
  2. ReactTransitionGroup : esta API de nivel inferior es excelente, pero requiere que use una devolución de llamada cuando se complete la animación, por lo que solo usar transiciones CSS no funcionará aquí. Siempre hay bibliotecas de animación, lo que lleva al siguiente punto:
  3. GreenSock: la licencia es demasiado restrictiva para el uso comercial de la OMI.
  4. Reaccionar movimiento: esto parece genial, pero TransitionMotion es extremadamente confuso y demasiado complicado para lo que necesito.
  5. Por supuesto, puedo hacer trucos como lo hace Material UI, donde los elementos se representan pero permanecen ocultos ( left: -10000px ) pero prefiero no seguir esa ruta. Lo considero hacky, y quiero que mis componentes se desmonten para que se limpien y no estén abarrotando el DOM.

Quiero algo que sea fácil de implementar. En la montura, anima un conjunto de estilos; al desmontar, anime el mismo (u otro) conjunto de estilos. Hecho. También tiene que ser de alto rendimiento en múltiples plataformas.

He golpeado una pared de ladrillos aquí. Si me falta algo y hay una manera fácil de hacerlo, avíseme.


Aquí está mi solución usando la nueva API de ganchos (con TypeScript), basada en esta publicación , para retrasar la fase de desmontaje del componente:

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

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

Enlace CodeSandbox .


Aquí mis 2cents: gracias a @deckele por su solución. Mi solución se basa en la suya, es la versión del componente con estado, totalmente reutilizable.

Aquí mi sandbox: https://codesandbox.io/s/302mkm1m .

aquí mi 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);


Así es como resolví esto en 2019, mientras hacía una rueda giratoria de carga. Estoy usando componentes funcionales React.

Tengo un componente de aplicación principal que tiene un componente secundario de Spinner .

La aplicación tiene un estado para saber si la aplicación se está cargando o no. Cuando se carga la aplicación, Spinner se procesa normalmente. Cuando la aplicación no se carga ( isLoading es falso) Spinner se renderiza con el accesorio debe 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 tiene estado para saber si está oculto o no. Al principio, con accesorios y estado predeterminados, Spinner se representa normalmente. La clase Spinner-fadeIn anima a desaparecer. Cuando Spinner recibe el accesorio debe shouldUnmount se renderiza con la clase Spinner-fadeOut , animándolo a desaparecer.

Sin embargo, también quería que el componente se desmontara después de desvanecerse.

En este punto, intenté usar el evento sintético onAnimationEnd React, similar a la solución de @ pranesh-ravi anterior, pero no funcionó. En su lugar, utilicé setTimeout para establecer el estado en oculto con un retraso de la misma duración que la animación. Spinner se actualizará después del retraso con isHidden === true , y no se mostrará nada.

La clave aquí es que el padre no desmonta al niño, le dice al niño cuándo desmontar, y el niño se desmonta a sí mismo después de encargarse de su negocio de desmontaje.

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


Contrarresté este problema durante mi trabajo y, por simple que parezca, realmente no está en React. En un escenario normal donde representa algo como:

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

a medida que this.state.show cambia, los niños se montan / desmontan de inmediato.

Un enfoque que tomé fue crear un componente de envoltura Animate y usarlo como

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

ahora que this.state.show cambia, podemos percibir cambios de prop con getDerivedStateFromProps(componentWillReceiveProps) y crear etapas de renderización intermedias para realizar animaciones.

Comenzamos con Static Stage cuando los niños están montados o desmontados.

Una vez que detectamos los cambios de la bandera de show , ingresamos a la Etapa de preparación, donde calculamos las propiedades necesarias como la height y el width de ReactDOM.findDOMNode.getBoundingClientRect() .

Luego, al ingresar al estado animado, podemos usar la transición CSS para cambiar la altura, el ancho y la opacidad de 0 a los valores calculados (o a 0 si se desmonta).

Al final de la transición, usamos la API onTransitionEnd para volver a la etapa Static .

Hay muchos más detalles sobre cómo las etapas se transfieren sin problemas, pero esta podría ser una idea general :)

Si alguien está interesado, creé una biblioteca React https://github.com/MingruiZhang/react-animate-mount para compartir mi solución. Cualquier comentario bienvenido :)


Creo que usar Transition from react-transition-group es probablemente la forma más fácil de seguir el montaje / desmontaje. Es increíblemente flexible. Estoy usando algunas clases para mostrar lo fácil que es usarlo, pero definitivamente puedes conectar tus propias animaciones JS utilizando el accesorio addEndListener , con el que también tuve mucha suerte usando GSAP.

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

Y aquí está mi 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);


Esto es un poco largo, pero he usado todos los eventos y métodos nativos para lograr esta animación. No ReactCSSTransitionGroup , ReactTransitionGroup y etc.

Cosas que he usado

  • Reaccionar los métodos del ciclo de vida.
  • evento onTransitionEnd

Como funciona esto

  • Monte el elemento en función del soporte de montaje pasado ( mounted ) y con el estilo predeterminado ( opacity: 0 )
  • Después de montar o actualizar, use componentDidMount ( componentWillReceiveProps para actualizaciones adicionales) para cambiar el estilo ( opacity: 1 ) con un tiempo de espera (para que sea asíncrono).
  • Durante el desmontaje, pase un accesorio al componente para identificar el desmontaje, cambie el estilo nuevamente ( opacity: 0 ), en onTransitionEnd , elimine desmonte el elemento del DOM.

Continúa el ciclo.

Revisa el código, lo entenderás. Si necesita alguna aclaración, deje un comentario.

Espero que esto ayude.

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>


Esto se puede hacer fácilmente usando el componente CSSTransition de react-transition-group , que es como las bibliotecas que mencionó. El truco es que necesita ajustar el componente CSSTransition sin un mecanismo de mostrar / ocultar como lo haría normalmente .ie {show && <Child>}... De lo contrario, está ocultando la animación y no funcionará. Ejemplo:

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


Para aquellos que consideran el movimiento de reacción, animar un solo componente cuando se monta y desmonta puede ser abrumador de configurar.

Hay una biblioteca llamada react-motion-ui-pack que hace que este proceso sea mucho más fácil de comenzar. Es una envoltura alrededor del movimiento de reacción, lo que significa que obtienes todos los beneficios de la biblioteca (es decir, puedes interrumpir la animación, tener múltiples desmontajes al mismo tiempo).

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 cuál debería ser el estado final del componente; dejar es el estilo que se aplica cuando el componente se desmonta.

Es posible que, una vez que haya usado el paquete de interfaz de usuario un par de veces, la biblioteca de react-motion ya no sea tan desalentadora.


También tenía una gran necesidad de animación de un solo componente. Estaba cansado de usar React Motion pero me estaba tirando de los pelos por un problema tan trivial ... (lo que digo). Después de buscar en Google, me encontré con esta publicación en su repositorio de git. Espero que ayude a alguien ...

Referenciado desde y también el crédito . Esto funciona para mí a partir de ahora. Mi caso de uso era un modo de animar y desmontar en caso de carga y descarga.

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


Usando el conocimiento obtenido de la respuesta de Pranesh, se me ocurrió una solución alternativa que es configurable y reutilizable:

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

Y finalmente, en el método de render otro componente:

return <div> <ReactTransitionGroup> <Thing /> </ReactTransitionGroup> </div>