@usefy/use-unmount 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @usefy/use-unmount
2
+
3
+ A React hook that executes a callback when the component unmounts.
4
+
5
+ ## Features
6
+
7
+ - **Closure Freshness**: Callback always has access to the latest state/props values
8
+ - **Error Handling**: Errors in callback are caught and don't break unmount
9
+ - **Conditional Execution**: Enable/disable cleanup via `enabled` option
10
+ - **SSR Compatible**: Works safely with server-side rendering
11
+ - **TypeScript Support**: Full type definitions included
12
+ - **Zero Dependencies**: Only peer dependency on React
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @usefy/use-unmount
18
+ # or
19
+ pnpm add @usefy/use-unmount
20
+ # or
21
+ yarn add @usefy/use-unmount
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Basic Usage
27
+
28
+ ```tsx
29
+ import { useUnmount } from "@usefy/use-unmount";
30
+
31
+ function MyComponent() {
32
+ useUnmount(() => {
33
+ console.log("Component unmounted");
34
+ });
35
+
36
+ return <div>Hello</div>;
37
+ }
38
+ ```
39
+
40
+ ### With Latest State Access
41
+
42
+ ```tsx
43
+ import { useState } from "react";
44
+ import { useUnmount } from "@usefy/use-unmount";
45
+
46
+ function FormComponent() {
47
+ const [formData, setFormData] = useState({});
48
+
49
+ useUnmount(() => {
50
+ // formData will have the latest value at unmount time
51
+ saveToLocalStorage(formData);
52
+ });
53
+
54
+ return <form>...</form>;
55
+ }
56
+ ```
57
+
58
+ ### Conditional Cleanup
59
+
60
+ ```tsx
61
+ import { useUnmount } from "@usefy/use-unmount";
62
+
63
+ function TrackingComponent({ trackingEnabled }) {
64
+ useUnmount(
65
+ () => {
66
+ sendAnalyticsEvent("component_unmounted");
67
+ },
68
+ { enabled: trackingEnabled }
69
+ );
70
+
71
+ return <div>Tracked content</div>;
72
+ }
73
+ ```
74
+
75
+ ### Resource Cleanup
76
+
77
+ ```tsx
78
+ import { useEffect, useRef } from "react";
79
+ import { useUnmount } from "@usefy/use-unmount";
80
+
81
+ function WebSocketComponent() {
82
+ const wsRef = useRef<WebSocket | null>(null);
83
+
84
+ useEffect(() => {
85
+ wsRef.current = new WebSocket("wss://example.com");
86
+ }, []);
87
+
88
+ useUnmount(() => {
89
+ wsRef.current?.close();
90
+ });
91
+
92
+ return <div>Connected</div>;
93
+ }
94
+ ```
95
+
96
+ ## API
97
+
98
+ ### `useUnmount(callback, options?)`
99
+
100
+ #### Parameters
101
+
102
+ | Name | Type | Description |
103
+ |------|------|-------------|
104
+ | `callback` | `() => void` | Function to execute when component unmounts |
105
+ | `options` | `UseUnmountOptions` | Optional configuration |
106
+
107
+ #### Options
108
+
109
+ | Name | Type | Default | Description |
110
+ |------|------|---------|-------------|
111
+ | `enabled` | `boolean` | `true` | Whether to execute callback on unmount |
112
+
113
+ ## React StrictMode
114
+
115
+ In development with React StrictMode, components are intentionally mounted, unmounted, and remounted to detect side effects. This means the unmount callback may be called multiple times during development. This is expected behavior.
116
+
117
+ ## Error Handling
118
+
119
+ If the callback throws an error, it will be caught and logged to the console. This prevents unmount errors from breaking the entire component tree unmount process.
120
+
121
+ ## When to Use
122
+
123
+ Use `useUnmount` when you need to:
124
+
125
+ - Save data before component removal
126
+ - Send analytics events on component exit
127
+ - Clean up resources that aren't managed by useEffect
128
+ - Log component lifecycle events
129
+ - Perform final state snapshots
130
+
131
+ ## When NOT to Use
132
+
133
+ Consider using `useEffect` cleanup instead when:
134
+
135
+ - Cleaning up subscriptions (use `useEffect` return function)
136
+ - Removing event listeners (use `useEventListener` hook)
137
+ - Canceling requests (use abort controllers in `useEffect`)
138
+
139
+ The key difference is that `useUnmount` guarantees access to the latest values, while `useEffect` cleanup captures values at effect creation time.
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Options for useUnmount hook
3
+ */
4
+ interface UseUnmountOptions {
5
+ /**
6
+ * Whether the unmount callback is enabled.
7
+ * When false, the callback will not be executed on unmount.
8
+ * @default true
9
+ */
10
+ enabled?: boolean;
11
+ }
12
+ /**
13
+ * Executes a callback function when the component unmounts.
14
+ * The callback always has access to the latest values (closure freshness).
15
+ *
16
+ * Features:
17
+ * - Closure freshness: callback always sees latest state/props
18
+ * - Error handling: errors in callback don't break unmount
19
+ * - Conditional execution via `enabled` option
20
+ * - SSR compatible
21
+ * - TypeScript support
22
+ *
23
+ * @param callback - The function to execute on unmount
24
+ * @param options - Configuration options
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * // Basic usage
29
+ * function MyComponent() {
30
+ * useUnmount(() => {
31
+ * console.log("Component unmounted");
32
+ * });
33
+ *
34
+ * return <div>Hello</div>;
35
+ * }
36
+ * ```
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * // With latest state access
41
+ * function FormComponent() {
42
+ * const [formData, setFormData] = useState({});
43
+ *
44
+ * useUnmount(() => {
45
+ * // formData will have the latest value at unmount time
46
+ * saveToLocalStorage(formData);
47
+ * });
48
+ *
49
+ * return <form>...</form>;
50
+ * }
51
+ * ```
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * // Conditional cleanup
56
+ * function TrackingComponent({ trackingEnabled }) {
57
+ * useUnmount(
58
+ * () => {
59
+ * sendAnalyticsEvent("component_unmounted");
60
+ * },
61
+ * { enabled: trackingEnabled }
62
+ * );
63
+ *
64
+ * return <div>Tracked content</div>;
65
+ * }
66
+ * ```
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * // Resource cleanup
71
+ * function WebSocketComponent() {
72
+ * const wsRef = useRef<WebSocket | null>(null);
73
+ *
74
+ * useEffect(() => {
75
+ * wsRef.current = new WebSocket("wss://example.com");
76
+ * }, []);
77
+ *
78
+ * useUnmount(() => {
79
+ * wsRef.current?.close();
80
+ * });
81
+ *
82
+ * return <div>Connected</div>;
83
+ * }
84
+ * ```
85
+ *
86
+ * @remarks
87
+ * **React StrictMode**: In development with StrictMode, React intentionally
88
+ * mounts, unmounts, and remounts components to detect side effects. This means
89
+ * the unmount callback may be called multiple times during development.
90
+ * This is expected behavior and helps identify issues with cleanup logic.
91
+ *
92
+ * **Error Handling**: If the callback throws an error, it will be caught and
93
+ * logged to the console. This prevents unmount errors from breaking
94
+ * the entire component tree unmount process.
95
+ */
96
+ declare function useUnmount(callback: () => void, options?: UseUnmountOptions): void;
97
+
98
+ export { type UseUnmountOptions, useUnmount };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Options for useUnmount hook
3
+ */
4
+ interface UseUnmountOptions {
5
+ /**
6
+ * Whether the unmount callback is enabled.
7
+ * When false, the callback will not be executed on unmount.
8
+ * @default true
9
+ */
10
+ enabled?: boolean;
11
+ }
12
+ /**
13
+ * Executes a callback function when the component unmounts.
14
+ * The callback always has access to the latest values (closure freshness).
15
+ *
16
+ * Features:
17
+ * - Closure freshness: callback always sees latest state/props
18
+ * - Error handling: errors in callback don't break unmount
19
+ * - Conditional execution via `enabled` option
20
+ * - SSR compatible
21
+ * - TypeScript support
22
+ *
23
+ * @param callback - The function to execute on unmount
24
+ * @param options - Configuration options
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * // Basic usage
29
+ * function MyComponent() {
30
+ * useUnmount(() => {
31
+ * console.log("Component unmounted");
32
+ * });
33
+ *
34
+ * return <div>Hello</div>;
35
+ * }
36
+ * ```
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * // With latest state access
41
+ * function FormComponent() {
42
+ * const [formData, setFormData] = useState({});
43
+ *
44
+ * useUnmount(() => {
45
+ * // formData will have the latest value at unmount time
46
+ * saveToLocalStorage(formData);
47
+ * });
48
+ *
49
+ * return <form>...</form>;
50
+ * }
51
+ * ```
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * // Conditional cleanup
56
+ * function TrackingComponent({ trackingEnabled }) {
57
+ * useUnmount(
58
+ * () => {
59
+ * sendAnalyticsEvent("component_unmounted");
60
+ * },
61
+ * { enabled: trackingEnabled }
62
+ * );
63
+ *
64
+ * return <div>Tracked content</div>;
65
+ * }
66
+ * ```
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * // Resource cleanup
71
+ * function WebSocketComponent() {
72
+ * const wsRef = useRef<WebSocket | null>(null);
73
+ *
74
+ * useEffect(() => {
75
+ * wsRef.current = new WebSocket("wss://example.com");
76
+ * }, []);
77
+ *
78
+ * useUnmount(() => {
79
+ * wsRef.current?.close();
80
+ * });
81
+ *
82
+ * return <div>Connected</div>;
83
+ * }
84
+ * ```
85
+ *
86
+ * @remarks
87
+ * **React StrictMode**: In development with StrictMode, React intentionally
88
+ * mounts, unmounts, and remounts components to detect side effects. This means
89
+ * the unmount callback may be called multiple times during development.
90
+ * This is expected behavior and helps identify issues with cleanup logic.
91
+ *
92
+ * **Error Handling**: If the callback throws an error, it will be caught and
93
+ * logged to the console. This prevents unmount errors from breaking
94
+ * the entire component tree unmount process.
95
+ */
96
+ declare function useUnmount(callback: () => void, options?: UseUnmountOptions): void;
97
+
98
+ export { type UseUnmountOptions, useUnmount };
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ useUnmount: () => useUnmount
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/useUnmount.ts
28
+ var import_react = require("react");
29
+ function useUnmount(callback, options = {}) {
30
+ const { enabled = true } = options;
31
+ const callbackRef = (0, import_react.useRef)(callback);
32
+ callbackRef.current = callback;
33
+ (0, import_react.useEffect)(() => {
34
+ if (!enabled) {
35
+ return;
36
+ }
37
+ return () => {
38
+ try {
39
+ callbackRef.current();
40
+ } catch (error) {
41
+ console.error("useUnmount: Error in unmount callback:", error);
42
+ }
43
+ };
44
+ }, [enabled]);
45
+ }
46
+ // Annotate the CommonJS export names for ESM import in node:
47
+ 0 && (module.exports = {
48
+ useUnmount
49
+ });
50
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/useUnmount.ts"],"sourcesContent":["export { useUnmount } from \"./useUnmount\";\nexport type { UseUnmountOptions } from \"./useUnmount\";\n","import { useEffect, useRef } from \"react\";\n\n/**\n * Options for useUnmount hook\n */\nexport interface UseUnmountOptions {\n /**\n * Whether the unmount callback is enabled.\n * When false, the callback will not be executed on unmount.\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Executes a callback function when the component unmounts.\n * The callback always has access to the latest values (closure freshness).\n *\n * Features:\n * - Closure freshness: callback always sees latest state/props\n * - Error handling: errors in callback don't break unmount\n * - Conditional execution via `enabled` option\n * - SSR compatible\n * - TypeScript support\n *\n * @param callback - The function to execute on unmount\n * @param options - Configuration options\n *\n * @example\n * ```tsx\n * // Basic usage\n * function MyComponent() {\n * useUnmount(() => {\n * console.log(\"Component unmounted\");\n * });\n *\n * return <div>Hello</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With latest state access\n * function FormComponent() {\n * const [formData, setFormData] = useState({});\n *\n * useUnmount(() => {\n * // formData will have the latest value at unmount time\n * saveToLocalStorage(formData);\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Conditional cleanup\n * function TrackingComponent({ trackingEnabled }) {\n * useUnmount(\n * () => {\n * sendAnalyticsEvent(\"component_unmounted\");\n * },\n * { enabled: trackingEnabled }\n * );\n *\n * return <div>Tracked content</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Resource cleanup\n * function WebSocketComponent() {\n * const wsRef = useRef<WebSocket | null>(null);\n *\n * useEffect(() => {\n * wsRef.current = new WebSocket(\"wss://example.com\");\n * }, []);\n *\n * useUnmount(() => {\n * wsRef.current?.close();\n * });\n *\n * return <div>Connected</div>;\n * }\n * ```\n *\n * @remarks\n * **React StrictMode**: In development with StrictMode, React intentionally\n * mounts, unmounts, and remounts components to detect side effects. This means\n * the unmount callback may be called multiple times during development.\n * This is expected behavior and helps identify issues with cleanup logic.\n *\n * **Error Handling**: If the callback throws an error, it will be caught and\n * logged to the console. This prevents unmount errors from breaking\n * the entire component tree unmount process.\n */\nexport function useUnmount(\n callback: () => void,\n options: UseUnmountOptions = {}\n): void {\n const { enabled = true } = options;\n\n // Store callback in ref to always have the latest version\n // This ensures closure freshness - the callback at unmount time\n // will have access to the most recent state/props values\n const callbackRef = useRef(callback);\n\n // Update the ref on every render to capture latest callback\n callbackRef.current = callback;\n\n useEffect(() => {\n // If disabled, don't set up cleanup\n if (!enabled) {\n return;\n }\n\n // Return cleanup function\n return () => {\n try {\n callbackRef.current();\n } catch (error) {\n // Log the error to help with debugging\n // Catch to prevent breaking other unmounts in the component tree\n console.error(\"useUnmount: Error in unmount callback:\", error);\n }\n };\n }, [enabled]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAkC;AAkG3B,SAAS,WACd,UACA,UAA6B,CAAC,GACxB;AACN,QAAM,EAAE,UAAU,KAAK,IAAI;AAK3B,QAAM,kBAAc,qBAAO,QAAQ;AAGnC,cAAY,UAAU;AAEtB,8BAAU,MAAM;AAEd,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAGA,WAAO,MAAM;AACX,UAAI;AACF,oBAAY,QAAQ;AAAA,MACtB,SAAS,OAAO;AAGd,gBAAQ,MAAM,0CAA0C,KAAK;AAAA,MAC/D;AAAA,IACF;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AACd;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,23 @@
1
+ // src/useUnmount.ts
2
+ import { useEffect, useRef } from "react";
3
+ function useUnmount(callback, options = {}) {
4
+ const { enabled = true } = options;
5
+ const callbackRef = useRef(callback);
6
+ callbackRef.current = callback;
7
+ useEffect(() => {
8
+ if (!enabled) {
9
+ return;
10
+ }
11
+ return () => {
12
+ try {
13
+ callbackRef.current();
14
+ } catch (error) {
15
+ console.error("useUnmount: Error in unmount callback:", error);
16
+ }
17
+ };
18
+ }, [enabled]);
19
+ }
20
+ export {
21
+ useUnmount
22
+ };
23
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useUnmount.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\n/**\n * Options for useUnmount hook\n */\nexport interface UseUnmountOptions {\n /**\n * Whether the unmount callback is enabled.\n * When false, the callback will not be executed on unmount.\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Executes a callback function when the component unmounts.\n * The callback always has access to the latest values (closure freshness).\n *\n * Features:\n * - Closure freshness: callback always sees latest state/props\n * - Error handling: errors in callback don't break unmount\n * - Conditional execution via `enabled` option\n * - SSR compatible\n * - TypeScript support\n *\n * @param callback - The function to execute on unmount\n * @param options - Configuration options\n *\n * @example\n * ```tsx\n * // Basic usage\n * function MyComponent() {\n * useUnmount(() => {\n * console.log(\"Component unmounted\");\n * });\n *\n * return <div>Hello</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With latest state access\n * function FormComponent() {\n * const [formData, setFormData] = useState({});\n *\n * useUnmount(() => {\n * // formData will have the latest value at unmount time\n * saveToLocalStorage(formData);\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Conditional cleanup\n * function TrackingComponent({ trackingEnabled }) {\n * useUnmount(\n * () => {\n * sendAnalyticsEvent(\"component_unmounted\");\n * },\n * { enabled: trackingEnabled }\n * );\n *\n * return <div>Tracked content</div>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Resource cleanup\n * function WebSocketComponent() {\n * const wsRef = useRef<WebSocket | null>(null);\n *\n * useEffect(() => {\n * wsRef.current = new WebSocket(\"wss://example.com\");\n * }, []);\n *\n * useUnmount(() => {\n * wsRef.current?.close();\n * });\n *\n * return <div>Connected</div>;\n * }\n * ```\n *\n * @remarks\n * **React StrictMode**: In development with StrictMode, React intentionally\n * mounts, unmounts, and remounts components to detect side effects. This means\n * the unmount callback may be called multiple times during development.\n * This is expected behavior and helps identify issues with cleanup logic.\n *\n * **Error Handling**: If the callback throws an error, it will be caught and\n * logged to the console. This prevents unmount errors from breaking\n * the entire component tree unmount process.\n */\nexport function useUnmount(\n callback: () => void,\n options: UseUnmountOptions = {}\n): void {\n const { enabled = true } = options;\n\n // Store callback in ref to always have the latest version\n // This ensures closure freshness - the callback at unmount time\n // will have access to the most recent state/props values\n const callbackRef = useRef(callback);\n\n // Update the ref on every render to capture latest callback\n callbackRef.current = callback;\n\n useEffect(() => {\n // If disabled, don't set up cleanup\n if (!enabled) {\n return;\n }\n\n // Return cleanup function\n return () => {\n try {\n callbackRef.current();\n } catch (error) {\n // Log the error to help with debugging\n // Catch to prevent breaking other unmounts in the component tree\n console.error(\"useUnmount: Error in unmount callback:\", error);\n }\n };\n }, [enabled]);\n}\n"],"mappings":";AAAA,SAAS,WAAW,cAAc;AAkG3B,SAAS,WACd,UACA,UAA6B,CAAC,GACxB;AACN,QAAM,EAAE,UAAU,KAAK,IAAI;AAK3B,QAAM,cAAc,OAAO,QAAQ;AAGnC,cAAY,UAAU;AAEtB,YAAU,MAAM;AAEd,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAGA,WAAO,MAAM;AACX,UAAI;AACF,oBAAY,QAAQ;AAAA,MACtB,SAAS,OAAO;AAGd,gBAAQ,MAAM,0CAA0C,KAAK;AAAA,MAC/D;AAAA,IACF;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AACd;","names":[]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@usefy/use-unmount",
3
+ "version": "0.0.20",
4
+ "description": "A React hook that executes a callback when the component unmounts",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "peerDependencies": {
20
+ "react": "^18.0.0 || ^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@testing-library/jest-dom": "^6.9.1",
24
+ "@testing-library/react": "^16.3.1",
25
+ "@types/react": "^19.0.0",
26
+ "jsdom": "^27.3.0",
27
+ "react": "^19.0.0",
28
+ "rimraf": "^6.0.1",
29
+ "tsup": "^8.0.0",
30
+ "typescript": "^5.0.0",
31
+ "vitest": "^4.0.16"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/geon0529/usefy.git",
39
+ "directory": "packages/use-unmount"
40
+ },
41
+ "license": "MIT",
42
+ "keywords": [
43
+ "react",
44
+ "hooks",
45
+ "unmount",
46
+ "cleanup",
47
+ "lifecycle",
48
+ "useUnmount",
49
+ "useEffect"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "typecheck": "tsc --noEmit",
57
+ "clean": "rimraf dist"
58
+ }
59
+ }