diff --git a/src/SwitchTransition.js b/src/SwitchTransition.js
new file mode 100644
index 00000000..e7714588
--- /dev/null
+++ b/src/SwitchTransition.js
@@ -0,0 +1,191 @@
+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
+ *
+ * ```jsx
+ *
+ * function App() {
+ * const [state, setState] = useState(false);
+ * return (
+ *
+ * node.addEventListener("transitionend", done, false)}
+ * classNames='fade' >
+ *
+ *
+ *
+ * )
+ * }
+ * ```
+ */
+export 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 (
+
+ {component}
+
+ )
+ }
+}
+
+
+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
+}
diff --git a/test/SwitchTransition-test.js b/test/SwitchTransition-test.js
new file mode 100644
index 00000000..833da0d7
--- /dev/null
+++ b/test/SwitchTransition-test.js
@@ -0,0 +1,129 @@
+import React from 'react';
+
+import { mount } from 'enzyme';
+
+import Transition, { ENTERED } from '../src/Transition';
+import { SwitchTransition, modes } from '../src/SwitchTransition';
+
+describe('SwitchTransition', () => {
+ let log, Parent;
+ beforeEach(() => {
+ log = [];
+ let events = {
+ onEnter: (_, m) => log.push(m ? 'appear' : 'enter'),
+ onEntering: (_, m) => log.push(m ? 'appearing' : 'entering'),
+ onEntered: (_, m) => log.push(m ? 'appeared' : 'entered'),
+ onExit: () => log.push('exit'),
+ onExiting: () => log.push('exiting'),
+ onExited: () => log.push('exited')
+ };
+
+ Parent = function Parent({ on, rendered = true }) {
+ return (
+
+ {rendered ? (
+
+
+
+ ) : null}
+
+ );
+ };
+ });
+
+ it('should have default status ENTERED', () => {
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(wrapper.state('status')).toBe(ENTERED);
+ });
+
+ it('should have default mode: out-in', () => {
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(wrapper.prop('mode')).toBe(modes.out);
+ });
+
+ it('should work without childs', () => {
+ expect(() => {
+ mount(
+
+
+
+
+
+ );
+ }).not.toThrow();
+ });
+
+ it('should switch between components on change state', () => {
+ const wrapper = mount();
+
+ jest.useFakeTimers();
+ expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe(
+ 'first'
+ );
+ wrapper.setProps({ on: false });
+ expect(log).toEqual(['exit', 'exiting']);
+ jest.runAllTimers();
+ expect(log).toEqual([
+ 'exit',
+ 'exiting',
+ 'exited',
+ 'enter',
+ 'entering',
+ 'entered'
+ ]);
+ expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe(
+ 'second'
+ );
+ });
+
+ it('should switch between null and component', () => {
+ const wrapper = mount();
+
+ expect(
+ wrapper.find(SwitchTransition).getElement().props.children
+ ).toBeFalsy();
+
+ jest.useFakeTimers();
+
+ wrapper.setProps({ rendered: true });
+ jest.runAllTimers();
+ expect(log).toEqual(['enter', 'entering', 'entered']);
+ expect(
+ wrapper.find(SwitchTransition).getElement().props.children
+ ).toBeTruthy();
+ expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe(
+ 'first'
+ );
+
+ wrapper.setProps({ on: false, rendered: true });
+ jest.runAllTimers();
+ expect(log).toEqual([
+ 'enter',
+ 'entering',
+ 'entered',
+ 'exit',
+ 'exiting',
+ 'exited',
+ 'enter',
+ 'entering',
+ 'entered'
+ ]);
+ expect(wrapper.find(SwitchTransition).getElement().props.children.key).toBe(
+ 'second'
+ );
+ });
+});