@winglet/react-utils
Version:
React utility library providing custom hooks, higher-order components (HOCs), and utility functions to enhance React application development with improved reusability and functionality
105 lines (104 loc) • 3.51 kB
TypeScript
import type { Fn } from '../@aileron/declare';
/**
* Executes a cleanup function when the component unmounts.
*
* This hook provides a semantic and intentional way to handle cleanup logic that should
* only run when a component is removed from the DOM. It offers cleaner syntax than
* remembering to return cleanup functions from `useEffect` and makes unmount logic explicit.
*
* ### Use Cases
* - **Resource Cleanup**: Cancel subscriptions, timers, or async operations
* - **Event Listener Removal**: Clean up global event listeners
* - **Connection Closure**: Close WebSocket, SSE, or database connections
* - **State Persistence**: Save component state before unmounting
* - **Analytics Tracking**: Record session duration or usage metrics
* - **Memory Management**: Clear caches, release large objects, or cleanup workers
*
* ### Critical Limitations
* - **Stale Closure Warning**: The handler function captures values at mount time only
* - **No State Updates**: Handler won't see later state or prop changes
* - **Single Execution**: Only runs on unmount, never on dependency changes
*
* ### Solutions for Current State Access
* Use `useReference` or `useRef` to access current values in cleanup:
*
* @example
* ```typescript
* // ❌ Problematic: captures stale state
* const [count, setCount] = useState(0);
* useOnUnmount(() => {
* console.log(count); // Always logs initial value (0)
* });
*
* // ✅ Correct: access current state
* const [count, setCount] = useState(0);
* const countRef = useReference(count);
* useOnUnmount(() => {
* console.log(countRef.current); // Logs current value
* });
*
* // Cancel ongoing requests with current context
* const controller = useRef(new AbortController());
* const userIdRef = useReference(userId);
*
* useOnUnmount(() => {
* controller.current.abort();
* analytics.track('UserSessionEnd', {
* userId: userIdRef.current,
* timestamp: Date.now()
* });
* });
*
* // Save draft with current form state
* const formDataRef = useReference(formData);
* const isDirtyRef = useReference(isDirty);
*
* useOnUnmount(() => {
* if (isDirtyRef.current) {
* localStorage.setItem('draft', JSON.stringify(formDataRef.current));
* }
* });
*
* // Third-party library cleanup
* const chartInstance = useRef<Chart>();
*
* useOnMount(() => {
* chartInstance.current = new Chart(canvasRef.current, config);
* });
*
* useOnUnmount(() => {
* chartInstance.current?.destroy();
* chartInstance.current = undefined;
* });
*
* // Multiple timer cleanup with Set
* const activeTimers = useRef(new Set<NodeJS.Timer>());
*
* const scheduleTimer = useCallback((callback: () => void, delay: number) => {
* const timer = setTimeout(() => {
* activeTimers.current.delete(timer);
* callback();
* }, delay);
* activeTimers.current.add(timer);
* return timer;
* }, []);
*
* useOnUnmount(() => {
* activeTimers.current.forEach(timer => clearTimeout(timer));
* activeTimers.current.clear();
* });
*
* // WebSocket with graceful shutdown
* const wsRef = useRef<WebSocket>();
* const connectionStateRef = useReference(connectionState);
*
* useOnUnmount(() => {
* if (wsRef.current && connectionStateRef.current === 'connected') {
* wsRef.current.close(1000, 'Component unmounting');
* }
* });
* ```
*
* @param handler - The cleanup function to execute when the component unmounts
*/
export declare const useOnUnmount: (handler: Fn) => void;