src/SwitchTransition.js (137 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import { ENTERED, ENTERING, EXITING } from './Transition'; import TransitionGroupContext from './TransitionGroupContext'; function areChildrenDifferent(oldChildren, newChildren) { if (oldChildren === newChildren) return false; if ( React.isValidElement(oldChildren) && React.isValidElement(newChildren) && oldChildren.key != null && oldChildren.key === newChildren.key ) { return false; } return true; } /** * Enum of modes for SwitchTransition component * @enum { string } */ export const modes = { out: 'out-in', in: 'in-out', }; const callHook = (element, name, cb) => (...args) => { element.props[name] && element.props[name](...args); cb(); }; const leaveRenders = { [modes.out]: ({ current, changeState }) => React.cloneElement(current, { in: false, onExited: callHook(current, 'onExited', () => { changeState(ENTERING, null); }), }), [modes.in]: ({ current, changeState, children }) => [ current, React.cloneElement(children, { in: true, onEntered: callHook(children, 'onEntered', () => { changeState(ENTERING); }), }), ], }; const enterRenders = { [modes.out]: ({ children, changeState }) => React.cloneElement(children, { in: true, onEntered: callHook(children, 'onEntered', () => { changeState(ENTERED, React.cloneElement(children, { in: true })); }), }), [modes.in]: ({ current, children, changeState }) => [ React.cloneElement(current, { in: false, onExited: callHook(current, 'onExited', () => { changeState(ENTERED, React.cloneElement(children, { in: true })); }), }), React.cloneElement(children, { in: true, }), ], }; /** * A transition component inspired by the [vue transition modes](https://vuejs.org/v2/guide/transitions.html#Transition-Modes). * You can use it when you want to control the render between state transitions. * Based on the selected mode and the child's key which is the `Transition` or `CSSTransition` component, the `SwitchTransition` makes a consistent transition between them. * * If the `out-in` mode is selected, the `SwitchTransition` waits until the old child leaves and then inserts a new child. * If the `in-out` mode is selected, the `SwitchTransition` inserts a new child first, waits for the new child to enter and then removes the old child. * * **Note**: If you want the animation to happen simultaneously * (that is, to have the old child removed and a new child inserted **at the same time**), * you should use * [`TransitionGroup`](https://reactcommunity.org/react-transition-group/transition-group) * instead. * * ```jsx * function App() { * const [state, setState] = useState(false); * return ( * <SwitchTransition> * <CSSTransition * key={state ? "Goodbye, world!" : "Hello, world!"} * addEndListener={(node, done) => node.addEventListener("transitionend", done, false)} * classNames='fade' * > * <button onClick={() => setState(state => !state)}> * {state ? "Goodbye, world!" : "Hello, world!"} * </button> * </CSSTransition> * </SwitchTransition> * ); * } * ``` * * ```css * .fade-enter{ * opacity: 0; * } * .fade-exit{ * opacity: 1; * } * .fade-enter-active{ * opacity: 1; * } * .fade-exit-active{ * opacity: 0; * } * .fade-enter-active, * .fade-exit-active{ * transition: opacity 500ms; * } * ``` */ class SwitchTransition extends React.Component { state = { status: ENTERED, current: null, }; appeared = false; componentDidMount() { this.appeared = true; } static getDerivedStateFromProps(props, state) { if (props.children == null) { return { current: null, }; } if (state.status === ENTERING && props.mode === modes.in) { return { status: ENTERING, }; } if (state.current && areChildrenDifferent(state.current, props.children)) { return { status: EXITING, }; } return { current: React.cloneElement(props.children, { in: true, }), }; } changeState = (status, current = this.state.current) => { this.setState({ status, current, }); }; render() { const { props: { children, mode }, state: { status, current }, } = this; const data = { children, current, changeState: this.changeState, status }; let component; switch (status) { case ENTERING: component = enterRenders[mode](data); break; case EXITING: component = leaveRenders[mode](data); break; case ENTERED: component = current; } return ( <TransitionGroupContext.Provider value={{ isMounting: !this.appeared }}> {component} </TransitionGroupContext.Provider> ); } } SwitchTransition.propTypes = { /** * Transition modes. * `out-in`: Current element transitions out first, then when complete, the new element transitions in. * `in-out`: New element transitions in first, then when complete, the current element transitions out. * * @type {'out-in'|'in-out'} */ mode: PropTypes.oneOf([modes.in, modes.out]), /** * Any `Transition` or `CSSTransition` component. */ children: PropTypes.oneOfType([PropTypes.element.isRequired]), }; SwitchTransition.defaultProps = { mode: modes.out, }; export default SwitchTransition;