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;