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:
ReactCSSTransitionGroup
- W ogóle nie używam klas CSS, to wszystkie style JS, więc to nie zadziała.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:- GreenSock - licencja jest zbyt restrykcyjna do użytku biznesowego IMO.
- React Motion - Wydaje się świetne, ale
TransitionMotion
jest bardzo zagmatwane i zbyt skomplikowane w stosunku do tego, czego potrzebuję. - 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ć.
transform: scale
.thing { color: #fff; }
)const styles = { thing: { color: '#fff' } }
Odpowiedzi:
To trochę za długie, ale wykorzystałem wszystkie natywne zdarzenia i metody, aby osiągnąć tę animację. Nie
ReactCSSTransitionGroup
,ReactTransitionGroup
itd.Rzeczy, których użyłem
onTransitionEnd
zdarzenieJak to działa
mounted
) iz domyślnym stylem (opacity: 0
)componentDidMount
(componentWillReceiveProps
dla dalszych aktualizacji), aby zmienić styl (opacity: 1
) z limitem czasu (aby uczynić go asynchronicznym).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>
źródło
onTransitionEnd
? Nie widzę tego w dokumentach React.componentWillReceiveProps
możesz coś zwrócić? Gdzie mogę przeczytać więcej na ten temat?Parent
komponencie odwołujesz się dothis.transitionEnd
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
render
metodzie innego komponentu :return <div> <ReactTransitionGroup> <Thing /> </ReactTransitionGroup> </div>
źródło
componentWillLeave()
i jakcomponentWillEnter()
zostać wezwanymAnimatedMount
?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 .
źródło
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.show
zmianami elementy podrzędne są montowane / odmontowywane od razu.Jedną z metod, które wybrałem, jest utworzenie komponentu opakowania
Animate
i używanie go w podobny sposób<Animate show={this.state.show}> {childen} </Animate>
teraz jako
this.state.show
zmiany możemy postrzegać zmiany rekwizytówgetDerivedStateFromProps(componentWillReceiveProps)
i tworzyć pośrednie etapy renderowania do wykonywania animacji.Zaczynamy od Static Stage, gdy dzieci są zamontowane lub odmontowane.
Po wykryciu
show
zmiany flagi wchodzimy do etapu przygotowawczego, w którym obliczamy niezbędne właściwości, takie jakheight
iwidth
odReactDOM.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
onTransitionEnd
interfejsu API, aby wrócić doStatic
etapu.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 :)
źródło
Myślę, że używanie
Transition
fromreact-transition-group
jest 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ąaddEndListener
rekwizytu - 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);
źródło
show
prop doH1
i wykonać całą logikę wewnątrz komponentu ze stylem. Jak ...animation: ${({ show }) => show ? entranceKeyframes : exitKeyframes} 300ms ease-out forwards;
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> )
źródło
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.
źródło
Animowanie przejść i wyjść jest znacznie łatwiejsze dzięki ruchowi reagującemu .
przykład na kodyandbox
źródło
Można to łatwo zrobić za pomocą
CSSTransition
komponentu z programureact-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; }
źródło
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);
źródło
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 (
isLoading
jest 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-fadeIn
Klasa ożywia to blaknięcie. Kiedy Spinner odbiera rekwizytshouldUnmount
to świadczy oSpinner-fadeOut
klasie zamiast animacji to wygaszanie.Jednak chciałem również, aby komponent odmontował się po wygaszeniu.
W tym momencie próbowałem użyć
onAnimationEnd
zdarzenia syntetycznego React, podobnego do rozwiązania @ pranesh-ravi powyżej, ale nie zadziałało. Zamiast tegosetTimeout
ustawiłem stan na ukryty z opóźnieniem o takiej samej długości jak animacja. Spinner zaktualizuje się po opóźnieniu za pomocąisHidden === true
i 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); } }
źródło
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> ); } }
źródło
Wiem, że jest tu wiele odpowiedzi, ale nadal nie znalazłem takiej, która odpowiadałaby moim potrzebom. Chcę:
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-bootstrap
to nazw klas i for<Container/>
, ale oba można łatwo zastąpić niestandardowym CSS i zwykłym starym<div>
.źródło
Jeśli używam biblioteki
Velocity
lubAnimeJS
do bezpośredniej animacji węzła (zamiastcss
lubsetTimeout
), dowiedziałem się, że mogę zaprojektowaćhook
stan animacjion
i funkcjęonToggle
uruchamiania 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żącoduration
./** * 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() }) }
źródło
Możesz do tego użyć React SyntheticEvent .
Dzięki zdarzeniom takim jak onAnimationEnd lub onTransitionEnd możesz to osiągnąć.
React Docs: https://reactjs.org/docs/events.html#animation-events
Przykład kodu: https://dev.to/michalczaplinski/super-easy-react-mount-unmount-animations-with-hooks-4foj
źródło
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-components
czy zwykłego css. Jest to szczególnie przydatne, gdy chcesz śledzić montaż i odmontowywanie komponentu i odpowiednio renderować animacje. UżywajTransition
ze styled-components iCSSTransition
gdy używasz zwykłych nazw klas CSS.źródło