useSnapshot
Creates a stable object reference that only changes when the object's contents actually change through deep comparison. This hook performs deep equality comparison to detect genuine content changes versus reference changes, returning the same object reference when contents are identical. It's essential for breaking the "new object every render" pattern that breaks memoization.
Core Problem it Solves
React components often create new objects on every render:
// These create new references even with identical content:
const config = { theme: 'dark', size: 'large' };
const user = { ...userData, isOnline: checkOnlineStatus() };
const settings = processSettings(rawSettings);
Even identical contents break useMemo, useCallback, and React.memo optimizations.
When to Use vs useRestProperties
- useSnapshot: Deep comparison for nested objects, complex data structures
- useRestProperties: Shallow comparison for flat objects, better performance
Deep Comparison Features
- Nested Object Support: Compares deeply nested properties
- Array Handling: Compares array contents and nested objects within arrays
- Property Exclusion: Skip volatile properties from comparison
- Null/Undefined Safety: Handles edge cases gracefully
- Type Preservation: Maintains TypeScript types perfectly
Performance Considerations
- Time Complexity: O(n) where n = total properties in object tree
- Memory: Stores one previous reference per hook instance
- Best for: Complex nested objects, API responses, configuration objects
- Avoid for: Large arrays, frequently changing data
Signature
const useSnapshot: <Input extends object | undefined>(input: Input, omit?: Set<keyof Input> | Array<keyof Input>) => Input
Parameters
| Name | Type | Description |
|---|---|---|
input | - | The object to create a deep-compared snapshot of |
omit | - | Properties to exclude from deep comparison (as Set or Array) |
Returns
A stable reference that only changes when object contents actually change
Examples
Example 1
// ❌ Problem: Effect runs on every render despite identical content
const MyComponent = ({ userData }) => {
const config = { theme: userData.theme, locale: userData.locale };
useEffect(() => {
initializeWidget(config); // Runs every render!
}, [config]);
};
// ✅ Solution: Stable reference for identical content
const MyComponent = ({ userData }) => {
const stableConfig = useSnapshot({
theme: userData.theme,
locale: userData.locale
});
useEffect(() => {
initializeWidget(stableConfig); // Only runs when content changes
}, [stableConfig]);
};
// Complex nested data stabilization
const UserProfile = ({ user, preferences, metadata }) => {
const stableUserData = useSnapshot({
personal: {
id: user.id,
name: user.name,
email: user.email
},
settings: {
theme: preferences.theme,
language: preferences.language,
notifications: {
email: preferences.notifications.email,
push: preferences.notifications.push
}
},
meta: {
lastLogin: metadata.lastLogin,
accountType: metadata.accountType
}
});
// Only recomputes when actual user data changes
const displayName = useMemo(() =>
formatUserName(stableUserData.personal), [stableUserData.personal]
);
return <ProfileDisplay data={stableUserData} name={displayName} />;
};
// API response caching with exclusions
const DataFetcher = ({ endpoint, params }) => {
const [response, setResponse] = useState(null);
// Exclude timestamp from comparison to prevent unnecessary requests
const stableResponse = useSnapshot(response, ['timestamp', 'requestId']);
const processedData = useMemo(() => {
if (!stableResponse) return null;
return expensiveTransform(stableResponse.data);
}, [stableResponse]);
return <DataDisplay data={processedData} />;
};
// Form state comparison for "dirty" detection
const FormEditor = ({ initialData }) => {
const [formData, setFormData] = useState(initialData);
const stableInitialData = useSnapshot(initialData);
const stableCurrentData = useSnapshot(formData);
// Accurate dirty detection based on content, not references
const isDirty = stableCurrentData !== stableInitialData;
const hasChanges = !equals(stableCurrentData, stableInitialData);
return (
<form>
<button disabled={!isDirty}>Save Changes</button>
</form>
);
};
// Context optimization with deep comparison
const AppStateProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [settings, setSettings] = useState({});
const [permissions, setPermissions] = useState([]);
const contextValue = useSnapshot({
user: {
...user,
isAuthenticated: !!user,
fullName: user ? `${user.firstName} ${user.lastName}` : ''
},
settings: {
...settings,
isDarkMode: settings.theme === 'dark'
},
permissions,
actions: {
login: setUser,
updateSettings: setSettings,
updatePermissions: setPermissions
}
});
// Only re-renders when state actually changes
return (
<AppContext.Provider value={contextValue}>
{children}
</AppContext.Provider>
);
};
// Comparing arrays with nested objects
const TodoList = ({ todos, filters }) => {
const stableFilteredTodos = useSnapshot(
todos
.filter(todo => matchesFilters(todo, filters))
.map(todo => ({
...todo,
isOverdue: new Date(todo.dueDate) < new Date()
}))
);
return (
<VirtualizedList
items={stableFilteredTodos} // Stable reference prevents scroll position reset
renderItem={({ item }) => <TodoItem todo={item} />}
/>
);
};
Playground
// ❌ Problem: Effect runs on every render despite identical content const MyComponent = ({ userData }) => { const config = { theme: userData.theme, locale: userData.locale }; useEffect(() => { initializeWidget(config); // Runs every render! }, [config]); }; // ✅ Solution: Stable reference for identical content const MyComponent = ({ userData }) => { const stableConfig = useSnapshot({ theme: userData.theme, locale: userData.locale }); useEffect(() => { initializeWidget(stableConfig); // Only runs when content changes }, [stableConfig]); }; // Complex nested data stabilization const UserProfile = ({ user, preferences, metadata }) => { const stableUserData = useSnapshot({ personal: { id: user.id, name: user.name, email: user.email }, settings: { theme: preferences.theme, language: preferences.language, notifications: { email: preferences.notifications.email, push: preferences.notifications.push } }, meta: { lastLogin: metadata.lastLogin, accountType: metadata.accountType } }); // Only recomputes when actual user data changes const displayName = useMemo(() => formatUserName(stableUserData.personal), [stableUserData.personal] ); return <ProfileDisplay data={stableUserData} name={displayName} />; }; // API response caching with exclusions const DataFetcher = ({ endpoint, params }) => { const [response, setResponse] = useState(null); // Exclude timestamp from comparison to prevent unnecessary requests const stableResponse = useSnapshot(response, ['timestamp', 'requestId']); const processedData = useMemo(() => { if (!stableResponse) return null; return expensiveTransform(stableResponse.data); }, [stableResponse]); return <DataDisplay data={processedData} />; }; // Form state comparison for "dirty" detection const FormEditor = ({ initialData }) => { const [formData, setFormData] = useState(initialData); const stableInitialData = useSnapshot(initialData); const stableCurrentData = useSnapshot(formData); // Accurate dirty detection based on content, not references const isDirty = stableCurrentData !== stableInitialData; const hasChanges = !equals(stableCurrentData, stableInitialData); return ( <form> <button disabled={!isDirty}>Save Changes</button> </form> ); }; // Context optimization with deep comparison const AppStateProvider = ({ children }) => { const [user, setUser] = useState(null); const [settings, setSettings] = useState({}); const [permissions, setPermissions] = useState([]); const contextValue = useSnapshot({ user: { ...user, isAuthenticated: !!user, fullName: user ? `${user.firstName} ${user.lastName}` : '' }, settings: { ...settings, isDarkMode: settings.theme === 'dark' }, permissions, actions: { login: setUser, updateSettings: setSettings, updatePermissions: setPermissions } }); // Only re-renders when state actually changes return ( <AppContext.Provider value={contextValue}> {children} </AppContext.Provider> ); }; // Comparing arrays with nested objects const TodoList = ({ todos, filters }) => { const stableFilteredTodos = useSnapshot( todos .filter(todo => matchesFilters(todo, filters)) .map(todo => ({ ...todo, isOverdue: new Date(todo.dueDate) < new Date() })) ); return ( <VirtualizedList items={stableFilteredTodos} // Stable reference prevents scroll position reset renderItem={({ item }) => <TodoItem todo={item} />} /> ); };