Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | 61x 61x 61x 61x 59x 61x 59x 61x 41x 41x 5x 36x 36x 3x | import { useEffect, useRef } from "react";
/**
* Options for useUnmount hook
*/
export interface UseUnmountOptions {
/**
* Whether the unmount callback is enabled.
* When false, the callback will not be executed on unmount.
* @default true
*/
enabled?: boolean;
}
/**
* Executes a callback function when the component unmounts.
* The callback always has access to the latest values (closure freshness).
*
* Features:
* - Closure freshness: callback always sees latest state/props
* - Error handling: errors in callback don't break unmount
* - Conditional execution via `enabled` option
* - SSR compatible
* - TypeScript support
*
* @param callback - The function to execute on unmount
* @param options - Configuration options
*
* @example
* ```tsx
* // Basic usage
* function MyComponent() {
* useUnmount(() => {
* console.log("Component unmounted");
* });
*
* return <div>Hello</div>;
* }
* ```
*
* @example
* ```tsx
* // With latest state access
* function FormComponent() {
* const [formData, setFormData] = useState({});
*
* useUnmount(() => {
* // formData will have the latest value at unmount time
* saveToLocalStorage(formData);
* });
*
* return <form>...</form>;
* }
* ```
*
* @example
* ```tsx
* // Conditional cleanup
* function TrackingComponent({ trackingEnabled }) {
* useUnmount(
* () => {
* sendAnalyticsEvent("component_unmounted");
* },
* { enabled: trackingEnabled }
* );
*
* return <div>Tracked content</div>;
* }
* ```
*
* @example
* ```tsx
* // Resource cleanup
* function WebSocketComponent() {
* const wsRef = useRef<WebSocket | null>(null);
*
* useEffect(() => {
* wsRef.current = new WebSocket("wss://example.com");
* }, []);
*
* useUnmount(() => {
* wsRef.current?.close();
* });
*
* return <div>Connected</div>;
* }
* ```
*
* @remarks
* **React StrictMode**: In development with StrictMode, React intentionally
* mounts, unmounts, and remounts components to detect side effects. This means
* the unmount callback may be called multiple times during development.
* This is expected behavior and helps identify issues with cleanup logic.
*
* **Error Handling**: 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.
*/
export function useUnmount(
callback: () => void,
options: UseUnmountOptions = {}
): void {
const { enabled = true } = options;
// Store callback in ref to always have the latest version.
// This ensures closure freshness - the callback at unmount time
// will have access to the most recent state/props values.
const callbackRef = useRef(callback);
// Track the latest `enabled` value in a ref so the cleanup can read it at
// unmount time without needing `enabled` in the effect's dependency array.
const enabledRef = useRef(enabled);
// Update the refs after each commit (post-commit convention) so the values
// are refreshed without re-running the unmount effect. Keeping the writes in
// an effect rather than during render is concurrent-safe: React may render a
// component without committing it, and we only want committed values here.
useEffect(() => {
callbackRef.current = callback;
});
useEffect(() => {
enabledRef.current = enabled;
});
// Empty dependency array: this effect runs once on mount and its cleanup runs
// ONLY on real unmount. Because `enabled` lives in a ref, toggling it while
// the component is still mounted never re-runs this effect, so the callback
// is never fired prematurely - it honors the `enabled` value at unmount time.
useEffect(() => {
return () => {
// Read the latest enabled value at unmount time.
if (!enabledRef.current) {
return;
}
try {
callbackRef.current();
} catch (error) {
// Log the error to help with debugging.
// Catch to prevent breaking other unmounts in the component tree.
console.error("useUnmount: Error in unmount callback:", error);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}
|