useSnapshotReference
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
- Type & Emptiness Check: Fast path for unchanged object types
- Deep Equality Comparison: Recursive comparison with exclusion support
- Reference Preservation: Returns same ref when contents identical
- Optimized Updates: Only updates ref.current when necessary
Signature
const useSnapshotReference: <Input extends object | undefined>(input: Input, omit?: Set<keyof Input> | Array<keyof Input>) => import("react").RefObject<Input>
Parameters
| Name | Type | Description |
|---|---|---|
input | - | The object to track with deep comparison |
omit | - | Properties to exclude from deep comparison (as Set or Array) |
Returns
A ref whose current value updates only when object contents actually change
Examples
Example 1
// ❌ 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;
};
Playground
// ❌ 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; };