diff --git a/packages/react-transition-presence/src/CrossFlow/CrossFlow.mdx b/packages/react-transition-presence/src/CrossFlow/CrossFlow.mdx
new file mode 100644
index 0000000..a437012
--- /dev/null
+++ b/packages/react-transition-presence/src/CrossFlow/CrossFlow.mdx
@@ -0,0 +1,57 @@
+import { Meta, Canvas, Controls } from '@storybook/blocks';
+import * as stories from './CrossFlow.stories';
+
+
+
+# CrossFlow
+
+` ` is a component that allows you to defer the unmount lifecycle of a component.
+
+The callback function used in `useBeforeUnmount` in components that are rendered in the context of
+` ` will be called just before a component is unmounted. The promises that are created
+using the callback are awaited before unmounting a component. In the CrossFlow component new
+children are immediately rendered.
+
+The most common use-case for ` ` is animations, but it's not limited to this use case.
+
+## Props
+
+### Children
+
+` ` accepts a single child, use a `Fragment` when you want to render multiple components
+on the root level. New children are rendered when the component type for the children (`
` to
+`
`) change, or when the key of the children changes (`
` to `null`).
+
+```tsx
+// Transition between two instances
+
+ {myBoolean ? : }
+
+
+// Transition between component types (new instances are automatically created)
+
+ {myBoolean ? : }
+
+```
+
+### onChildrenMounted
+
+The `onChildrenMounted` is called when the new children are mounted.
+
+```tsx
+onChildrenMounted?: () => void;
+
+ console.log('onChildrenMounted')}>
+ ...
+
+```
+
+## Demo
+
+### Basic
+
+
+
+### Basic with Fragments
+
+
diff --git a/packages/react-transition-presence/src/CrossFlow/CrossFlow.stories.tsx b/packages/react-transition-presence/src/CrossFlow/CrossFlow.stories.tsx
new file mode 100644
index 0000000..d577ac9
--- /dev/null
+++ b/packages/react-transition-presence/src/CrossFlow/CrossFlow.stories.tsx
@@ -0,0 +1,144 @@
+/* eslint-disable react/no-multi-comp, react/jsx-no-literals,no-console */
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { useAnimation } from '@mediamonks/react-animation';
+import type { Meta } from '@storybook/react';
+import gsap from 'gsap';
+import { Fragment, useEffect, useRef, useState, type ReactElement } from 'react';
+import { useBeforeUnmount } from '../useBeforeUnmount/useBeforeUnmount.js';
+import { CrossFlow } from './CrossFlow.js';
+
+export default {
+ title: 'components/CrossFlow',
+ argTypes: {
+ children: {
+ description: 'ReactElement | null',
+ type: 'string',
+ },
+ onChildrenMounted: {
+ description: '() => void | undefined',
+ type: 'string',
+ },
+ },
+} satisfies Meta;
+
+type ChildProps = {
+ background: string;
+ duration?: number;
+ delayIn?: number;
+ onClick?(): void;
+};
+
+function Child({ background, onClick, duration = 1, delayIn = 0 }: ChildProps): ReactElement {
+ const ref = useRef(null);
+
+ // show when mounted/unmounted
+ useEffect(() => {
+ console.log('mounted', background);
+
+ return () => {
+ console.log('unmounted', background);
+ };
+ }, [background]);
+
+ useAnimation(() => {
+ console.log('animate-in', background);
+ return gsap.fromTo(ref.current, { opacity: 0 }, { opacity: 1, duration, delay: delayIn });
+ }, []);
+
+ // show visible animation during "before unmount" lifecycle
+ useBeforeUnmount(async (abortSignal) => {
+ console.log('animate-out', background);
+
+ const animation = gsap.fromTo(ref.current, { opacity: 1 }, { opacity: 0, duration });
+
+ abortSignal.addEventListener('abort', () => {
+ animation.pause(0);
+ });
+
+ return animation;
+ });
+
+ return (
+
+ );
+}
+
+export function CrossFlowExample(): ReactElement {
+ const [hue, setHue] = useState(0);
+
+ return (
+ <>
+
+ {
+ setHue(hue + ((90 + Math.random() * 270) % 360));
+ }}
+ />
+
+
+ Click the square to create a new element
+ >
+ );
+}
+
+export function CrossFlowFragmentExample(): ReactElement {
+ const [hue, setHue] = useState(0);
+
+ return (
+ <>
+
+
+ {
+ setHue(hue + ((90 + Math.random() * 270) % 360));
+ }}
+ />
+ {
+ setHue(hue + ((90 + Math.random() * 270) % 360));
+ }}
+ />
+
+
+
+ Click the square to create a new element
+ >
+ );
+}
diff --git a/packages/react-transition-presence/src/CrossFlow/CrossFlow.tsx b/packages/react-transition-presence/src/CrossFlow/CrossFlow.tsx
new file mode 100644
index 0000000..b609c62
--- /dev/null
+++ b/packages/react-transition-presence/src/CrossFlow/CrossFlow.tsx
@@ -0,0 +1,90 @@
+import { cloneElement, useCallback, useEffect, useRef, useState, type ReactElement } from 'react';
+import {
+ TransitionPresence,
+ type TransitionPresenceProps,
+} from '../TransitionPresence/TransitionPresence.js';
+import { useIsMounted } from '../_hooks/useIsMounted.js';
+import { childrenAreEqual } from '../_utils/childrenAreEqual.js';
+import { getId } from '../_utils/getId.js';
+
+type CrossFlowQueue = Record;
+
+type CrossFlowProps = TransitionPresenceProps;
+
+export function CrossFlow({
+ children,
+ onChildrenMounted,
+ ...props
+}: TransitionPresenceProps): ReactElement {
+ const previousChildren = useRef(children);
+ const [queue, setQueue] = useState(() =>
+ children === null
+ ? {}
+ : {
+ [getId()]: children,
+ },
+ );
+
+ const isMounted = useIsMounted();
+
+ useEffect(() => {
+ // Don't do anything during the first render
+ if (!isMounted.current) {
+ return;
+ }
+
+ // If the children are equal we don't want to do anything
+ const areChildrenEqual = childrenAreEqual(children, previousChildren.current);
+ previousChildren.current = children;
+
+ if (areChildrenEqual) {
+ return;
+ }
+
+ // Add new children to the queue and remove the old children instance for
+ // its key to start the out transition
+ setQueue((value) => {
+ const emptyQueue: Record = {};
+
+ // eslint-disable-next-line guard-for-in
+ for (const key in value) {
+ emptyQueue[key] = null;
+ }
+
+ if (children === null) {
+ return emptyQueue;
+ }
+
+ return {
+ ...emptyQueue,
+ [getId()]: children,
+ };
+ });
+ }, [children, isMounted, previousChildren]);
+
+ const onChildrenMountedCleanup = useCallback(
+ (_children: ReactElement) => {
+ setQueue((value) => {
+ if ('key' in _children && _children.key !== null) {
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete value[_children.key];
+ }
+
+ return { ...value };
+ });
+
+ onChildrenMounted?.(_children, null);
+ },
+ [onChildrenMounted],
+ );
+
+ return (
+ <>
+ {Object.entries(queue).map(([key, queuedChildren]) => (
+
+ {queuedChildren && cloneElement(queuedChildren, { key })}
+
+ ))}
+ >
+ );
+}
diff --git a/packages/react-transition-presence/src/TransitionPresence/TransitionPresence.mdx b/packages/react-transition-presence/src/TransitionPresence/TransitionPresence.mdx
index 77ff6c6..3b03e58 100644
--- a/packages/react-transition-presence/src/TransitionPresence/TransitionPresence.mdx
+++ b/packages/react-transition-presence/src/TransitionPresence/TransitionPresence.mdx
@@ -5,11 +5,13 @@ import * as stories from './TransitionPresence.stories';
# TransitionPresence
-` ` is a component that allows you to defer unmount of a component.
+` ` is a component that allows you to defer the unmount lifecycle of a
+component.
The callback function used in `useBeforeUnmount` in components that are rendered in the context of
` ` will be called just before a component is unmounted. The promises that are
-created using the callback are await before unmounting a component.
+created using the callback are awaited before unmounting a component. New children are rendered
+after old children are unmounted.
The most common use-case for ` ` is animations, but it's not limited to this
use case.
diff --git a/packages/react-transition-presence/src/TransitionPresence/TransitionPresence.tsx b/packages/react-transition-presence/src/TransitionPresence/TransitionPresence.tsx
index be3d939..9cf4cf9 100644
--- a/packages/react-transition-presence/src/TransitionPresence/TransitionPresence.tsx
+++ b/packages/react-transition-presence/src/TransitionPresence/TransitionPresence.tsx
@@ -25,7 +25,10 @@ export type TransitionPresenceProps = {
previousChildren: ReactElement | null,
children: ReactElement | null,
): void | Promise;
- onChildrenMounted?(children: ReactElement | null): void | Promise;
+ onChildrenMounted?(
+ previousChildren: ReactElement | null,
+ children: ReactElement | null,
+ ): void | Promise;
};
/**
@@ -84,7 +87,7 @@ export function TransitionPresence({
setPreviousChildren(children);
await tick();
- onChildrenMountedRef.current?.(children);
+ onChildrenMountedRef.current?.(previousChildren, children);
})();
return () => {
diff --git a/packages/react-transition-presence/src/_hooks/useIsMounted.ts b/packages/react-transition-presence/src/_hooks/useIsMounted.ts
new file mode 100644
index 0000000..b4e0574
--- /dev/null
+++ b/packages/react-transition-presence/src/_hooks/useIsMounted.ts
@@ -0,0 +1,17 @@
+import { useEffect, useRef, type RefObject } from 'react';
+
+export function useIsMounted(): RefObject {
+ const isMounted = useRef(false);
+
+ useEffect(() => {
+ queueMicrotask(() => {
+ isMounted.current = true;
+ });
+
+ return () => {
+ isMounted.current = false;
+ };
+ }, []);
+
+ return isMounted;
+}
diff --git a/packages/react-transition-presence/src/_utils/getId.ts b/packages/react-transition-presence/src/_utils/getId.ts
new file mode 100644
index 0000000..e54ba36
--- /dev/null
+++ b/packages/react-transition-presence/src/_utils/getId.ts
@@ -0,0 +1,5 @@
+let id = 0;
+
+export function getId(): string {
+ return (id++ % Number.MAX_SAFE_INTEGER).toString();
+}