@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
137 lines (136 loc) • 4.75 kB
TypeScript
import type { Fn } from '../@aileron/declare';
/**
* Executes a cleanup function synchronously when the component unmounts, before browser painting.
*
* This hook is the synchronous version of `useOnUnmount`, using `useLayoutEffect` to ensure
* cleanup runs before the browser reflows or repaints. This prevents visual glitches, layout
* shifts, and DOM inconsistencies during component removal.
*
* ### When to Use Over useOnUnmount
* - **Prevent Visual Flicker**: Remove DOM nodes before layout recalculation
* - **Animation Cleanup**: Cancel in-progress animations before next frame
* - **Global Style Restoration**: Reset document/body styles before paint
* - **Portal Management**: Remove portal containers before DOM updates
* - **Synchronous Library APIs**: Clean up libraries that require immediate DOM cleanup
*
* ### Performance Warning
* **This blocks browser painting** - use sparingly and only when synchronous cleanup
* is essential to prevent visual artifacts. For most cleanup, prefer `useOnUnmount`.
*
* ### Critical Limitations (Same as useOnUnmount)
* - **Stale Closure Warning**: Handler captures values at mount time only
* - **No State Updates**: Handler won't see later state or prop changes
* - Use `useReference` for accessing current state in cleanup
*
* @example
* ```typescript
* // Portal cleanup to prevent layout shift
* const portalRoot = useRef<HTMLDivElement>();
*
* useOnMountLayout(() => {
* portalRoot.current = document.createElement('div');
* portalRoot.current.className = 'modal-portal';
* document.body.appendChild(portalRoot.current);
* });
*
* useOnUnmountLayout(() => {
* // Must remove synchronously to prevent layout issues
* portalRoot.current?.remove();
* });
*
* // Stop animations before component removal
* const animatingElementsRef = useReference(animatingElements);
*
* useOnUnmountLayout(() => {
* animatingElementsRef.current.forEach(element => {
* element.style.animation = 'none';
* element.style.transition = 'none';
* element.getAnimations().forEach(anim => anim.cancel());
* });
* });
*
* // Body scroll lock with synchronous restoration
* const originalBodyStylesRef = useRef<{
* overflow: string;
* position: string;
* touchAction: string;
* }>();
*
* useOnMountLayout(() => {
* const body = document.body;
* originalBodyStylesRef.current = {
* overflow: body.style.overflow,
* position: body.style.position,
* touchAction: body.style.touchAction,
* };
*
* body.style.overflow = 'hidden';
* body.style.position = 'fixed';
* body.style.touchAction = 'none';
* });
*
* useOnUnmountLayout(() => {
* const body = document.body;
* const original = originalBodyStylesRef.current;
* if (original) {
* body.style.overflow = original.overflow;
* body.style.position = original.position;
* body.style.touchAction = original.touchAction;
* }
* });
*
* // Drag-and-drop state cleanup before paint
* const dragStateRef = useReference(dragState);
*
* useOnUnmountLayout(() => {
* // Remove all drag-related DOM elements
* document.querySelectorAll('.drag-ghost, .drop-indicator')
* .forEach(el => el.remove());
*
* // Reset global cursor and selection
* document.body.style.cursor = '';
* document.body.classList.remove('dragging');
* window.getSelection()?.removeAllRanges();
*
* // Clear drag data if still active
* if (dragStateRef.current.isActive) {
* dragStateRef.current.cleanup();
* }
* });
*
* // Synchronous editor cleanup (prevents memory leaks)
* const editorInstanceRef = useRef<CodeMirror.Editor>();
*
* useOnUnmountLayout(() => {
* const editor = editorInstanceRef.current;
* if (editor) {
* // Some editors require synchronous cleanup to prevent errors
* editor.toTextArea(); // Restore original textarea
* editor.getWrapperElement().remove(); // Remove DOM immediately
* editorInstanceRef.current = undefined;
* }
* });
*
* // WebGL context cleanup before reflow
* const canvasRef = useRef<HTMLCanvasElement>();
* const glContextRef = useRef<WebGLRenderingContext>();
*
* useOnUnmountLayout(() => {
* const gl = glContextRef.current;
* if (gl) {
* // Synchronously release WebGL resources
* const extension = gl.getExtension('WEBGL_lose_context');
* extension?.loseContext();
*
* // Clear canvas immediately
* if (canvasRef.current) {
* canvasRef.current.width = 1;
* canvasRef.current.height = 1;
* }
* }
* });
* ```
*
* @param handler - The cleanup function to execute synchronously when the component unmounts
*/
export declare const useOnUnmountLayout: (handler: Fn) => void;