(null);
+
+ useEffect(() => {
+ if (!ref.current) {
+ return;
+ }
+
+ const animation = ref.current.animate([{ opacity: 0 }, { opacity: 1 }], {
+ duration: 500,
+ });
+
+ return () => {
+ animation.cancel();
+ };
+ }, []);
+
+ // show visible animation during "before unmount" lifecycle
+ useBeforeUnmount(async (abortSignal) => {
+ if (!ref.current) {
+ return;
+ }
+
+ const animation = ref.current.animate([{ opacity: 1 }, { opacity: 0 }], {
+ duration: 500,
+ fill: 'forwards',
+ });
+
+ abortSignal.addEventListener('abort', () => {
+ animation.cancel();
+ });
+
+ await animation.finished;
+ });
+
+ return (
+
+ );
+}
+
+export const Demo: Story = {
+ render(): ReactElement {
+ const [isRedVisible, setIsRedVisible] = useState(true);
+
+ return (
+ <>
+
+ {isRedVisible ? (
+ {
+ setIsRedVisible(false);
+ }}
+ />
+ ) : (
+ {
+ setIsRedVisible(true);
+ }}
+ />
+ )}
+
+
+ Click the square (isRedVisible: {String(isRedVisible)})
+ >
+ );
+ },
+ async play({ canvasElement }) {
+ const canvas = within(canvasElement);
+
+ expect(canvas.getByTestId('red')).toBeInTheDocument();
+ expect(canvas.queryByTestId('blue')).toBeNull();
+
+ await userEvent.click(canvas.getByTestId('red'));
+
+ expect(canvas.getByTestId('red')).toBeInTheDocument();
+ expect(canvas.queryByTestId('blue')).toBeNull();
+
+ const blue = await canvas.findByTestId('blue');
+
+ expect(blue).toBeInTheDocument();
+ expect(canvas.queryByTestId('red')).toBeNull();
+
+ await userEvent.click(canvas.getByTestId('blue'));
+
+ expect(canvas.getByTestId('blue')).toBeInTheDocument();
+ expect(canvas.queryByTestId('red')).toBeNull();
+
+ const red = await canvas.findByTestId('red');
+
+ expect(red).toBeInTheDocument();
+ expect(canvas.queryByTestId('blue')).toBeNull();
+ },
+};
+
+export const DemoUsingFragment: Story = {
+ render(): ReactElement {
+ const [isRedVisible, setIsRedVisible] = useState(true);
+
+ return (
+ <>
+
+ {isRedVisible ? (
+
+ {
+ setIsRedVisible(false);
+ }}
+ />
+
+
+ ) : (
+
+
+ {
+ setIsRedVisible(true);
+ }}
+ />
+
+ )}
+
+
+
+ Click the blue/red square (isRedVisible: {String(isRedVisible)})
+
+ >
+ );
+ },
+ async play({ canvasElement }) {
+ const canvas = within(canvasElement);
+
+ expect(canvas.getByTestId('red')).toBeInTheDocument();
+ expect(canvas.queryByTestId('blue')).toBeNull();
+
+ await userEvent.click(canvas.getByTestId('red'));
+
+ expect(canvas.getByTestId('red')).toBeInTheDocument();
+ expect(canvas.queryByTestId('blue')).toBeNull();
+
+ const blue = await canvas.findByTestId('blue');
+
+ expect(blue).toBeInTheDocument();
+ expect(canvas.queryByTestId('red')).toBeNull();
+
+ await userEvent.click(canvas.getByTestId('blue'));
+
+ expect(canvas.getByTestId('blue')).toBeInTheDocument();
+ expect(canvas.queryByTestId('red')).toBeNull();
+
+ const red = await canvas.findByTestId('red');
+
+ expect(red).toBeInTheDocument();
+ expect(canvas.queryByTestId('blue')).toBeNull();
+ },
+};
+
+export const DemoWithLifecycleCallbacks = {
+ render(): ReactElement {
+ const [isRedVisible, setIsRedVisible] = useState(true);
+
+ const onPreviousChildrenUnmounting = useCallback(() => {
+ // eslint-disable-next-line no-console
+ console.log('onPreviousChildrenUnmounting');
+ }, []);
+
+ const onPreviousChildrenUnmounted = useCallback(() => {
+ // eslint-disable-next-line no-console
+ console.log('onPreviousChildrenUnmounted');
+ }, []);
+
+ const onChildrenMounted = useCallback(() => {
+ // eslint-disable-next-line no-console
+ console.log('onChildrenMounted');
+ }, []);
+
+ return (
+ <>
+
+ {isRedVisible ? (
+ {
+ setIsRedVisible(false);
+ }}
+ />
+ ) : (
+ {
+ setIsRedVisible(true);
+ }}
+ />
+ )}
+
+ Click the square (isRedVisible: {String(isRedVisible)})
+ >
+ );
+ },
+};
+
+export const DemoClickToHide: Story = {
+ render(): ReactElement {
+ const [isVisible, setIsVisible] = useState(true);
+
+ return (
+ <>
+
+ {isVisible ? (
+ {
+ setIsVisible(false);
+ }}
+ />
+ ) : null}
+
+ Click the square (isVisible: {String(isVisible)})
+ >
+ );
+ },
+ async play({ canvasElement }) {
+ const canvas = within(canvasElement);
+ expect(canvas.getByTestId('red')).toBeInTheDocument();
+ await userEvent.click(canvas.getByTestId('red'), {
+ delay: 600,
+ });
+ expect(canvas.queryByTestId('red')).toBeNull();
+ },
+};
diff --git a/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx b/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx
new file mode 100644
index 0000000..6fbd2b2
--- /dev/null
+++ b/src/lifecycle/components/TransitionPresence/TransitionPresence.tsx
@@ -0,0 +1,115 @@
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactElement,
+ type RefObject,
+} from 'react';
+import { childrenAreEqual } from '../../../_utils/childrenAreEqual.js';
+import { useRefValue } from '../../../hooks/useRefValue/useRefValue.js';
+import { createTimeout } from '../../../index.js';
+import {
+ useTransitionPresenceBeforeUnmount,
+ type BeforeUnmountCallback,
+} from '../../hooks/useBeforeUnmount/useBeforeUnmount.js';
+import { TransitionPresenceContext } from './TransitionPresence.context.js';
+
+export type TransitionPresenceProps = {
+ children: ReactElement | null;
+ onPreviousChildrenUnmounting?(
+ previousChildren: ReactElement | null,
+ children: ReactElement | null,
+ ): void | Promise;
+ onPreviousChildrenUnmounted?(
+ previousChildren: ReactElement | null,
+ children: ReactElement | null,
+ ): void | Promise;
+ onChildrenMounted?(
+ previousChildren: ReactElement | null,
+ children: ReactElement | null,
+ ): void | Promise;
+};
+
+/**
+ * Will defer transition in new children by waiting on the
+ * `BeforeUnmountCallback`s that are registered using the `useBeforeUnmount`
+ * hook.
+ */
+export function TransitionPresence({
+ children,
+ onPreviousChildrenUnmounting,
+ onPreviousChildrenUnmounted,
+ onChildrenMounted,
+}: TransitionPresenceProps): ReactElement {
+ const beforeUnmountCallbacks = useMemo(() => new Set>(), []);
+ const [previousChildren, setPreviousChildren] = useState(children);
+
+ const onPreviousChildrenUnmountingRef = useRefValue(onPreviousChildrenUnmounting);
+ const onPreviousChildrenUnmountedRef = useRefValue(onPreviousChildrenUnmounted);
+ const onChildrenMountedRef = useRefValue(onChildrenMounted);
+
+ const beforeUnmountPreviousChildren = useCallback(
+ async (abortSignal: AbortSignal) => {
+ const promises: Array> = [];
+
+ for (const callback of beforeUnmountCallbacks) {
+ promises.push(callback.current?.(abortSignal));
+ }
+
+ await Promise.all(promises);
+ },
+ [beforeUnmountCallbacks],
+ );
+
+ useEffect(() => {
+ if (childrenAreEqual(children, previousChildren)) {
+ setPreviousChildren(children);
+ return;
+ }
+
+ const abortController = new AbortController();
+
+ (async (): Promise => {
+ onPreviousChildrenUnmountingRef.current?.(previousChildren, children);
+
+ // Defer children update for before unmount lifecycle
+ await beforeUnmountPreviousChildren(abortController.signal);
+
+ setPreviousChildren(null);
+
+ // Wait a tick after removing previous children to make sure new children
+ // are re-initialized
+ await createTimeout();
+
+ onPreviousChildrenUnmountedRef.current?.(previousChildren, children);
+
+ // Set new children
+ setPreviousChildren(children);
+ await createTimeout();
+
+ onChildrenMountedRef.current?.(previousChildren, children);
+ })();
+
+ return () => {
+ abortController.abort();
+ };
+ }, [
+ beforeUnmountCallbacks,
+ beforeUnmountPreviousChildren,
+ children,
+ onChildrenMountedRef,
+ onPreviousChildrenUnmountedRef,
+ onPreviousChildrenUnmountingRef,
+ previousChildren,
+ ]);
+
+ // Apply same effect when TransitionPresence in tree updates
+ useTransitionPresenceBeforeUnmount(beforeUnmountPreviousChildren);
+
+ return (
+
+ {previousChildren}
+
+ );
+}
diff --git a/src/hooks/useBeforeMount/useBeforeMount.mdx b/src/lifecycle/hooks/useBeforeMount/useBeforeMount.mdx
similarity index 94%
rename from src/hooks/useBeforeMount/useBeforeMount.mdx
rename to src/lifecycle/hooks/useBeforeMount/useBeforeMount.mdx
index 8f5b630..afdb094 100644
--- a/src/hooks/useBeforeMount/useBeforeMount.mdx
+++ b/src/lifecycle/hooks/useBeforeMount/useBeforeMount.mdx
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/blocks';
-
+
# useBeforeMount
diff --git a/src/hooks/useBeforeMount/useBeforeMount.test.tsx b/src/lifecycle/hooks/useBeforeMount/useBeforeMount.test.tsx
similarity index 100%
rename from src/hooks/useBeforeMount/useBeforeMount.test.tsx
rename to src/lifecycle/hooks/useBeforeMount/useBeforeMount.test.tsx
diff --git a/src/hooks/useBeforeMount/useBeforeMount.ts b/src/lifecycle/hooks/useBeforeMount/useBeforeMount.ts
similarity index 100%
rename from src/hooks/useBeforeMount/useBeforeMount.ts
rename to src/lifecycle/hooks/useBeforeMount/useBeforeMount.ts
diff --git a/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.mdx b/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.mdx
new file mode 100644
index 0000000..77dca71
--- /dev/null
+++ b/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.mdx
@@ -0,0 +1,78 @@
+import { Canvas, Meta } from '@storybook/blocks';
+import * as transitionPrescenseStories from '../../components/TransitionPresence/TransitionPresence.stories';
+
+
+
+# useBeforeUnmount
+
+The `useBeforeUnmount` hook accepts a function that will be called before the component is
+unmounted. The callback function is awaited, it defers the unmounting of the component until the
+function is resolved when it is wrapped in a `TransitionPresence` or `CrossFlow` context.
+
+```tsx
+function useBeforeUnmount(
+ callback: (abortSignal: AbortSignal) => PromiseLike | void,
+): void;
+```
+
+## Waiting for an animation before unmounting
+
+A common use case for the `useBeforeUnmount` hook is to create an out transition that defers the
+unmounting of the component until the transition is complete.
+
+
+
+The out transition in the demo is created using the Animation API.
+
+```tsx
+useBeforeUnmount(async () => {
+ if (!ref.current) {
+ return;
+ }
+
+ const animation = ref.current.animate([{ opacity: 1 }, { opacity: 0 }], {
+ duration: 500,
+ fill: 'forwards',
+ });
+
+ return animation.finished;
+});
+```
+
+## How to deal with forced unmounting
+
+A component instance can be prematurely unmounted in two situations. First, when the
+`TransitionPresence` context is unmounted. Second, when a new direct child is rendered in
+`TransitionPresence` before the callback of the previous child has resolved. To handle this
+situation, you can use the `AbortSignal` to cancel any running actions.
+
+### Cancelling an animation
+
+```tsx
+useBeforeUnmount(async (abortSignal) => {
+ if (!ref.current) {
+ return;
+ }
+
+ const animation = ref.current.animate([{ opacity: 1 }, { opacity: 0 }], {
+ duration: 500,
+ fill: 'forwards',
+ });
+
+ abortSignal.addEventListener('abort', () => {
+ animation.cancel();
+ });
+
+ await animation.finished;
+});
+```
+
+### Cancelling a request
+
+```tsx
+useBeforeUnmount(async (abortSignal) => {
+ await fetch('https://example.com/', {
+ signal: abortSignal,
+ });
+});
+```
diff --git a/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.stories.tsx b/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.stories.tsx
new file mode 100644
index 0000000..0228d54
--- /dev/null
+++ b/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.stories.tsx
@@ -0,0 +1,102 @@
+/* eslint-disable react-hooks/rules-of-hooks, react/jsx-no-literals */
+import { expect } from '@storybook/jest';
+import type { Meta, StoryObj } from '@storybook/react';
+import { userEvent, within } from '@storybook/testing-library';
+import { useState, type ReactElement, useRef } from 'react';
+import { TransitionPresence } from '../../components/TransitionPresence/TransitionPresence.js';
+import { useBeforeUnmount } from './useBeforeUnmount.js';
+
+const meta = {
+ title: 'Lifecycle / hooks / useBeforeUnmount',
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+type ChildProps = {
+ background: string;
+ onClick(): void;
+};
+
+function Child({ background, onClick }: ChildProps): ReactElement {
+ const ref = useRef(null);
+
+ useBeforeUnmount(async () => {
+ if (!ref.current) {
+ return;
+ }
+
+ const animation = ref.current.animate([{ opacity: 1 }, { opacity: 0 }], {
+ duration: 500,
+ fill: 'forwards',
+ });
+
+ await animation.finished;
+ });
+
+ return (
+
+ );
+}
+
+export const UseBeforeUnmount: Story = {
+ render() {
+ const [isRedVisible, setIsRedVisible] = useState(true);
+
+ return (
+ <>
+
+ {isRedVisible ? (
+ {
+ setIsRedVisible(false);
+ }}
+ />
+ ) : (
+ {
+ setIsRedVisible(true);
+ }}
+ />
+ )}
+
+ Click the square (isRedVisible: {String(isRedVisible)})
+ >
+ );
+ },
+ async play({ canvasElement }) {
+ const canvas = within(canvasElement);
+
+ const redButton = canvas.getByRole('button');
+ expect(redButton).toHaveStyle({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'background-color': 'rgb(255, 0, 0)',
+ });
+
+ await userEvent.click(redButton, { delay: 1000 });
+
+ const blueButton = canvas.getByRole('button');
+ expect(blueButton).toHaveStyle({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'background-color': 'rgb(0, 0, 255)',
+ });
+ },
+};
diff --git a/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.ts b/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.ts
new file mode 100644
index 0000000..b92e2eb
--- /dev/null
+++ b/src/lifecycle/hooks/useBeforeUnmount/useBeforeUnmount.ts
@@ -0,0 +1,49 @@
+import { useContext, useEffect } from 'react';
+import { useRefValue } from '../../../hooks/useRefValue/useRefValue.js';
+import { TransitionPresenceContext } from '../../components/TransitionPresence/TransitionPresence.context.js';
+
+// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
+export type BeforeUnmountCallback = (abortSignal: AbortSignal) => PromiseLike | void;
+
+/**
+ * Executes async callback to defer unmounting of children in nearest
+ * TransitionPresence boundary
+ */
+export function useBeforeUnmount(callback: BeforeUnmountCallback): void {
+ const transitionPresence = useContext(TransitionPresenceContext);
+ const callbackRef = useRefValue(callback);
+
+ if (transitionPresence === undefined) {
+ // eslint-disable-next-line no-console
+ console.warn('Component is not rendered in the context of a TransitionPresence');
+ }
+
+ useEffect(() => {
+ queueMicrotask(() => {
+ transitionPresence?.add(callbackRef);
+ });
+
+ return () => {
+ transitionPresence?.delete(callbackRef);
+ };
+ }, [transitionPresence, callbackRef]);
+}
+
+/**
+ * useBeforeUnmount without the warning, this should only be used within the
+ * component in this package.
+ */
+export function useTransitionPresenceBeforeUnmount(callback: BeforeUnmountCallback): void {
+ const transitionPresence = useContext(TransitionPresenceContext);
+ const callbackRef = useRefValue(callback);
+
+ useEffect(() => {
+ queueMicrotask(() => {
+ transitionPresence?.add(callbackRef);
+ });
+
+ return () => {
+ transitionPresence?.delete(callbackRef);
+ };
+ }, [transitionPresence, callbackRef]);
+}
diff --git a/src/hooks/useIsMounted/useIsMounted.mdx b/src/lifecycle/hooks/useIsMounted/useIsMounted.mdx
similarity index 98%
rename from src/hooks/useIsMounted/useIsMounted.mdx
rename to src/lifecycle/hooks/useIsMounted/useIsMounted.mdx
index 41b5ca8..d60b6f4 100644
--- a/src/hooks/useIsMounted/useIsMounted.mdx
+++ b/src/lifecycle/hooks/useIsMounted/useIsMounted.mdx
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/blocks';
-
+
# useIsMounted
diff --git a/src/hooks/useIsMounted/useIsMounted.stories.tsx b/src/lifecycle/hooks/useIsMounted/useIsMounted.stories.tsx
similarity index 83%
rename from src/hooks/useIsMounted/useIsMounted.stories.tsx
rename to src/lifecycle/hooks/useIsMounted/useIsMounted.stories.tsx
index c2605da..4a1b2a7 100644
--- a/src/hooks/useIsMounted/useIsMounted.stories.tsx
+++ b/src/lifecycle/hooks/useIsMounted/useIsMounted.stories.tsx
@@ -1,13 +1,17 @@
/* eslint-disable react/jsx-no-literals,react/jsx-handler-names,no-console */
-import type { StoryObj } from '@storybook/react';
+import type { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react';
-import { useForceRerender } from '../useForceRerender/useForceRerender.js';
+import { useForceRerender } from '../../../hooks/useForceRerender/useForceRerender.js';
import { useIsMountedState } from '../useIsMountedState/useIsMountedState.js';
import { useIsMounted } from './useIsMounted.js';
-export default {
- title: 'hooks/lifecycle/useIsMounted',
-};
+const meta = {
+ title: 'Lifecycle / hooks / useIsMounted',
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
function DemoComponent(): JSX.Element {
const forceRerender = useForceRerender();
@@ -51,7 +55,7 @@ function DemoComponent(): JSX.Element {
);
}
-export const Demo: StoryObj = {
+export const Demo: Story = {
render() {
return ;
},
diff --git a/src/hooks/useIsMounted/useIsMounted.test.tsx b/src/lifecycle/hooks/useIsMounted/useIsMounted.test.tsx
similarity index 100%
rename from src/hooks/useIsMounted/useIsMounted.test.tsx
rename to src/lifecycle/hooks/useIsMounted/useIsMounted.test.tsx
diff --git a/src/hooks/useIsMounted/useIsMounted.ts b/src/lifecycle/hooks/useIsMounted/useIsMounted.ts
similarity index 100%
rename from src/hooks/useIsMounted/useIsMounted.ts
rename to src/lifecycle/hooks/useIsMounted/useIsMounted.ts
diff --git a/src/hooks/useIsMountedState/useIsMountedState.mdx b/src/lifecycle/hooks/useIsMountedState/useIsMountedState.mdx
similarity index 97%
rename from src/hooks/useIsMountedState/useIsMountedState.mdx
rename to src/lifecycle/hooks/useIsMountedState/useIsMountedState.mdx
index ba99639..6b8b4ea 100644
--- a/src/hooks/useIsMountedState/useIsMountedState.mdx
+++ b/src/lifecycle/hooks/useIsMountedState/useIsMountedState.mdx
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/blocks';
-
+
# useIsMountedState
diff --git a/src/hooks/useIsMountedState/useIsMountedState.stories.tsx b/src/lifecycle/hooks/useIsMountedState/useIsMountedState.stories.tsx
similarity index 80%
rename from src/hooks/useIsMountedState/useIsMountedState.stories.tsx
rename to src/lifecycle/hooks/useIsMountedState/useIsMountedState.stories.tsx
index c0d5090..3c8de83 100644
--- a/src/hooks/useIsMountedState/useIsMountedState.stories.tsx
+++ b/src/lifecycle/hooks/useIsMountedState/useIsMountedState.stories.tsx
@@ -1,12 +1,16 @@
/* eslint-disable react/jsx-no-literals,react/jsx-handler-names,no-console */
-import type { StoryObj } from '@storybook/react';
+import type { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react';
-import { useForceRerender } from '../useForceRerender/useForceRerender.js';
+import { useForceRerender } from '../../../hooks/useForceRerender/useForceRerender.js';
import { useIsMountedState } from './useIsMountedState.js';
-export default {
- title: 'hooks/lifecycle/useIsMountedState',
-};
+const meta = {
+ title: 'Lifecycle / hooks / useIsMountedState',
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
function DemoComponent(): JSX.Element {
const forceRerender = useForceRerender();
@@ -42,7 +46,7 @@ function DemoComponent(): JSX.Element {
);
}
-export const Demo: StoryObj = {
+export const Demo: Story = {
render() {
return ;
},
diff --git a/src/hooks/useIsMountedState/useIsMountedState.test.tsx b/src/lifecycle/hooks/useIsMountedState/useIsMountedState.test.tsx
similarity index 100%
rename from src/hooks/useIsMountedState/useIsMountedState.test.tsx
rename to src/lifecycle/hooks/useIsMountedState/useIsMountedState.test.tsx
diff --git a/src/hooks/useIsMountedState/useIsMountedState.ts b/src/lifecycle/hooks/useIsMountedState/useIsMountedState.ts
similarity index 100%
rename from src/hooks/useIsMountedState/useIsMountedState.ts
rename to src/lifecycle/hooks/useIsMountedState/useIsMountedState.ts
diff --git a/src/hooks/useMount/useMount.mdx b/src/lifecycle/hooks/useMount/useMount.mdx
similarity index 91%
rename from src/hooks/useMount/useMount.mdx
rename to src/lifecycle/hooks/useMount/useMount.mdx
index 2c0f4fb..8276ca1 100644
--- a/src/hooks/useMount/useMount.mdx
+++ b/src/lifecycle/hooks/useMount/useMount.mdx
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/blocks';
-
+
# useMount
diff --git a/src/hooks/useMount/useMount.test.tsx b/src/lifecycle/hooks/useMount/useMount.test.tsx
similarity index 100%
rename from src/hooks/useMount/useMount.test.tsx
rename to src/lifecycle/hooks/useMount/useMount.test.tsx
diff --git a/src/hooks/useMount/useMount.ts b/src/lifecycle/hooks/useMount/useMount.ts
similarity index 100%
rename from src/hooks/useMount/useMount.ts
rename to src/lifecycle/hooks/useMount/useMount.ts
diff --git a/src/hooks/useUnmount/useUnmount.mdx b/src/lifecycle/hooks/useUnmount/useUnmount.mdx
similarity index 91%
rename from src/hooks/useUnmount/useUnmount.mdx
rename to src/lifecycle/hooks/useUnmount/useUnmount.mdx
index 92b89d7..de95c7b 100644
--- a/src/hooks/useUnmount/useUnmount.mdx
+++ b/src/lifecycle/hooks/useUnmount/useUnmount.mdx
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/blocks';
-
+
# useUnmount
diff --git a/src/hooks/useUnmount/useUnmount.test.tsx b/src/lifecycle/hooks/useUnmount/useUnmount.test.tsx
similarity index 100%
rename from src/hooks/useUnmount/useUnmount.test.tsx
rename to src/lifecycle/hooks/useUnmount/useUnmount.test.tsx
diff --git a/src/hooks/useUnmount/useUnmount.ts b/src/lifecycle/hooks/useUnmount/useUnmount.ts
similarity index 87%
rename from src/hooks/useUnmount/useUnmount.ts
rename to src/lifecycle/hooks/useUnmount/useUnmount.ts
index 7079828..5fb6a18 100644
--- a/src/hooks/useUnmount/useUnmount.ts
+++ b/src/lifecycle/hooks/useUnmount/useUnmount.ts
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
-import { useRefValue } from '../useRefValue/useRefValue.js';
+import { useRefValue } from '../../../hooks/useRefValue/useRefValue.js';
/**
* React lifecycle hook that calls a function after the component is unmounted.
diff --git a/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.mdx b/src/nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.mdx
similarity index 96%
rename from src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.mdx
rename to src/nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.mdx
index 119d9e4..c0eb9eb 100644
--- a/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.mdx
+++ b/src/nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.mdx
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/blocks';
-
+
# useIsomorphicLayoutEffect
diff --git a/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.tsx b/src/nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.tsx
similarity index 100%
rename from src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.tsx
rename to src/nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.tsx
diff --git a/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts b/src/nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts
similarity index 100%
rename from src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts
rename to src/nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts