I’ve implemented persistent custom filters across multiple HubSpot installations. Let me address all three critical components: filter state persistence, localStorage usage best practices, and MutationObserver implementation for dynamic DOM handling.
Filter State Persistence: Your code saves the filter but doesn’t restore it on page load. The deal board loads asynchronously, so you need a complete persistence cycle:
// Save filter state
function saveFilterState(criteria) {
const filterState = {
criteria: criteria,
timestamp: Date.now(),
boardView: getCurrentBoardView()
};
localStorage.setItem('hs_custom_deal_filter', JSON.stringify(filterState));
}
// Restore filter state
function restoreFilterState() {
const saved = localStorage.getItem('hs_custom_deal_filter');
if (saved) {
const state = JSON.parse(saved);
// Check if saved state is recent (within 7 days)
if (Date.now() - state.timestamp < 7 * 24 * 60 * 60 * 1000) {
return state.criteria;
}
}
return null;
}
localStorage Usage: Use a namespaced key to avoid conflicts with HubSpot’s own storage. Add version tracking so you can invalidate old filter formats if your criteria structure changes:
const FILTER_VERSION = '1.0';
const STORAGE_KEY = 'hs_custom_deal_filter_v' + FILTER_VERSION;
Store minimal data. Don’t save the entire board state, just the filter criteria that can reconstruct the view. This prevents quota issues and improves performance.
MutationObserver for Dynamic DOM: The deal board renders progressively. You must wait for complete initialization before applying filters:
function initializeFilterPersistence() {
const savedFilter = restoreFilterState();
if (!savedFilter) return;
const observer = new MutationObserver((mutations, obs) => {
const dealBoard = document.querySelector('.deal-board-wrapper');
const dealsLoaded = document.querySelectorAll('.deal-card').length > 0;
if (dealBoard && dealsLoaded) {
obs.disconnect();
setTimeout(() => applyCustomFilter(savedFilter), 500);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Fallback timeout if MutationObserver doesn't detect the board
setTimeout(() => observer.disconnect(), 10000);
}
window.addEventListener('load', initializeFilterPersistence);
The 500ms delay after disconnecting the observer is crucial. The deal board might be present in the DOM but still initializing its internal state. Applying filters immediately can cause them to be overridden.
For user efficiency, add visual feedback during filter restoration:
function applyCustomFilter(criteria) {
showFilterRestoreIndicator();
// Apply filter logic
updateBoardView(criteria);
// Save for next session
saveFilterState(criteria);
setTimeout(hideFilterRestoreIndicator, 1000);
}
Handle edge cases: user logs out (clear filters), workspace switches (namespace by workspace), and concurrent tabs (use storage events to sync):
window.addEventListener('storage', (e) => {
if (e.key === STORAGE_KEY && e.newValue !== e.oldValue) {
const newFilter = JSON.parse(e.newValue);
applyCustomFilter(newFilter.criteria);
}
});
Test thoroughly with HubSpot’s own filters active. Some native filter combinations conflict with custom filters. Document which built-in filters work alongside your custom implementation.
This complete solution ensures filters persist reliably across sessions while handling all the timing and DOM readiness challenges of the hs-2022 deal board. Your reps will maintain their custom views indefinitely, significantly improving their workflow efficiency.