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 .
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á:
-
ReactCSSTransitionGroup
: no estoy usando clases CSS, son todos estilos JS, por lo que esto no funcionará. -
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: - GreenSock: la licencia es demasiado restrictiva para el uso comercial de la OMI.
-
Reaccionar movimiento: esto parece genial, pero
TransitionMotion
es extremadamente confuso y demasiado complicado para lo que necesito. -
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
), enonTransitionEnd
, 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>