UNPKG

@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

212 lines (211 loc) 8.06 kB
/** * Creates a ref containing a deep-compared snapshot that only updates when object contents actually change. * * This hook performs deep equality comparison and returns a stable ref object whose * `current` value only changes when the object's contents genuinely change. Unlike * `useSnapshot`, this provides a ref for imperative access, callback stability, * and integration with external APIs that expect refs. * * ### When to Use vs useSnapshot * - **useSnapshotReference**: Need ref object, imperative access, external API integration * - **useSnapshot**: Direct value access, simpler syntax, most common use case * * ### Key Benefits of Ref-Based Approach * - **Callback Stability**: Ref reference never changes, perfect for stable callbacks * - **Imperative Access**: Access current value in timers, event handlers, cleanup * - **External Library Integration**: Pass stable refs to non-React code * - **Performance Monitoring**: Track actual changes separate from re-renders * - **Cleanup Functions**: Access latest state in cleanup without dependencies * * ### Deep Comparison Algorithm * 1. **Type & Emptiness Check**: Fast path for unchanged object types * 2. **Deep Equality Comparison**: Recursive comparison with exclusion support * 3. **Reference Preservation**: Returns same ref when contents identical * 4. **Optimized Updates**: Only updates ref.current when necessary * * @example * ```typescript * // ❌ Problem: Callback recreated on every render * const DataProcessor = ({ complexData, onProcess }) => { * const processData = useCallback(() => { * // This callback recreates whenever complexData changes * const result = expensiveComputation(complexData); * onProcess(result); * }, [complexData, onProcess]); * * return <Worker onMessage={processData} />; * }; * * // ✅ Solution: Stable callback with current data access * const DataProcessor = ({ complexData, onProcess }) => { * const dataRef = useSnapshotReference(complexData); * const onProcessRef = useReference(onProcess); * * const processData = useCallback(() => { * // Callback reference never changes, but accesses current data * const result = expensiveComputation(dataRef.current); * onProcessRef.current(result); * }, [dataRef]); // dataRef reference never changes * * return <Worker onMessage={processData} />; * }; * * // Performance monitoring: separate renders from content changes * const PerformanceTracker = ({ data }) => { * const dataRef = useSnapshotReference(data); * const renderCount = useRef(0); * const changeCount = useRef(0); * const lastChangeTime = useRef(Date.now()); * * // Count every render * useEffect(() => { * renderCount.current++; * }); * * // Count only actual data changes * useEffect(() => { * changeCount.current++; * const now = Date.now(); * const timeSinceLastChange = now - lastChangeTime.current; * lastChangeTime.current = now; * * console.log(`Data change #${changeCount.current} after ${timeSinceLastChange}ms`); * console.log(`Efficiency: ${changeCount.current}/${renderCount.current} renders had actual changes`); * }, [dataRef]); * * return <div>Monitoring data changes...</div>; * }; * * // External library integration with stable config * const ChartComponent = ({ data, options }) => { * const canvasRef = useRef<HTMLCanvasElement>(null); * const chartInstanceRef = useRef<Chart>(); * const configRef = useSnapshotReference({ * data, * options, * responsive: true, * maintainAspectRatio: false * }); * * useEffect(() => { * if (canvasRef.current) { * // Create chart with stable config reference * chartInstanceRef.current = new Chart(canvasRef.current, configRef.current); * } * * return () => { * // Access current config in cleanup * const currentConfig = configRef.current; * if (currentConfig.options.saveOnDestroy) { * chartInstanceRef.current?.toBase64Image(); * } * chartInstanceRef.current?.destroy(); * }; * }, [configRef]); // Only recreates when config content changes * * return <canvas ref={canvasRef} />; * }; * * // WebSocket message handling with content-based processing * const MessageProcessor = ({ websocketMessage }) => { * // Exclude volatile fields from comparison * const messageRef = useSnapshotReference(websocketMessage, [ * 'timestamp', * 'sequenceNumber', * 'receivedAt' * ]); * * const processedDataRef = useRef(null); * * useEffect(() => { * const message = messageRef.current; * if (!message) return; * * // Only reprocess when message content actually changes * console.log('Processing new message content:', message.type); * processedDataRef.current = processMessage(message); * * // Trigger side effects * updateUI(processedDataRef.current); * logMessage(message.type, message.data); * }, [messageRef]); * * return <MessageDisplay data={processedDataRef.current} />; * }; * * // State transition tracking * const StateTransitionLogger = ({ appState }) => { * const currentStateRef = useSnapshotReference(appState); * const previousStateRef = useRef(currentStateRef.current); * * useEffect(() => { * const current = currentStateRef.current; * const previous = previousStateRef.current; * * if (previous && current !== previous) { * const changes = detectChanges(previous, current); * logStateTransition({ * from: previous, * to: current, * changes, * timestamp: Date.now() * }); * } * * previousStateRef.current = current; * }, [currentStateRef]); * * return null; // This is a logging-only component * }; * * // Imperative handle with stable data access * const DataEditor = React.forwardRef(({ initialData, validation }, ref) => { * const [currentData, setCurrentData] = useState(initialData); * const dataRef = useSnapshotReference(currentData); * const validationRef = useSnapshotReference(validation); * * useImperativeHandle(ref, () => ({ * getData: () => dataRef.current, * validate: () => validateData(dataRef.current, validationRef.current), * isDirty: () => dataRef.current !== initialData, * reset: () => setCurrentData(initialData), * getChanges: () => diffData(initialData, dataRef.current) * }), [dataRef, validationRef, initialData]); * * return ( * <div> * <DataForm data={currentData} onChange={setCurrentData} /> * </div> * ); * }); * * // Timer/interval with current state access * const AutoSaver = ({ formData, onSave }) => { * const formDataRef = useSnapshotReference(formData); * const onSaveRef = useReference(onSave); * * useEffect(() => { * const interval = setInterval(() => { * // Access current form data without recreating interval * const currentData = formDataRef.current; * if (currentData && currentData.isDirty) { * onSaveRef.current(currentData); * } * }, 30000); // Auto-save every 30 seconds * * return () => clearInterval(interval); * }, [formDataRef]); // Interval only recreates when form structure changes * * return null; * }; * ``` * * @typeParam Input - The type of the object to create a snapshot reference of (can be undefined) * @param input - The object to track with deep comparison * @param omit - Properties to exclude from deep comparison (as Set or Array) * @returns A ref whose current value updates only when object contents actually change * * @see useSnapshot - For direct value access without ref wrapper (most common use case) * @see useReference - For always-current refs without comparison (different use case) */ export declare const useSnapshotReference: <Input extends object | undefined>(input: Input, omit?: Set<keyof Input> | Array<keyof Input>) => import("react").RefObject<Input>;