All files / src/hooks useLocalStorage.ts

90% Statements 27/30
78.57% Branches 11/14
100% Functions 7/7
90% Lines 27/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131                                                                                                                        116x   62x       62x   62x   62x     3x 3x           116x 33x   33x 33x       33x 33x 33x   1x       33x                 116x 62x       62x 3x 2x 2x   1x           62x   62x 62x       116x    
import { useState, useEffect, useCallback } from 'react';
 
/**
 * Custom hook to sync state with localStorage
 * 
 * ## Business Perspective
 * 
 * Enables widgets to remember user preferences and settings across browser
 * sessions, improving user experience by maintaining personalized configurations.
 * This is particularly valuable for security officers who configure specific
 * security levels and want their settings persisted. 💾
 * 
 * ## Technical Perspective
 * 
 * Provides a React state hook that automatically syncs with localStorage,
 * handling JSON serialization/deserialization and SSR compatibility. Uses
 * the same API as useState for familiarity.
 * 
 * @template T - Type of the stored value
 * @param key - localStorage key to use for storage
 * @param initialValue - Initial value if key doesn't exist in localStorage
 * @returns Tuple of [storedValue, setValue] similar to useState
 * 
 * @example
 * ```tsx
 * // Store user's preferred theme
 * const [theme, setTheme] = useLocalStorage('theme', 'light');
 * 
 * return (
 *   <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
 *     Toggle Theme
 *   </button>
 * );
 * ```
 * 
 * @example
 * ```tsx
 * // Store security level preferences
 * const [savedLevels, setSavedLevels] = useLocalStorage('securityLevels', {
 *   availability: 'Moderate',
 *   integrity: 'Moderate',
 *   confidentiality: 'Moderate'
 * });
 * ```
 * 
 * @example
 * ```tsx
 * // Store widget visibility preferences
 * const [widgetPrefs, setWidgetPrefs] = useLocalStorage('widgetPreferences', {
 *   showDetails: true,
 *   expandedSections: ['overview', 'metrics']
 * });
 * ```
 */
export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  // State to store our value
  // Pass initial state function to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState<T>(() => {
    // SSR safety: return initial value if window is undefined
    Iif (typeof window === 'undefined') {
      return initialValue;
    }
    
    try {
      // Get from local storage by key
      const item = window.localStorage.getItem(key);
      // Parse stored json or return initialValue if none exists
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      // If error reading from localStorage, log and return initial value
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });
 
  // Return a wrapped version of useState's setter function that
  // persists the new value to localStorage
  const setValue = useCallback((value: T | ((prev: T) => T)) => {
    try {
      // Allow value to be a function like useState
      setStoredValue((prevValue) => {
        const valueToStore = value instanceof Function ? value(prevValue) : value;
        
        // Save to local storage - wrapped in try/catch to handle QuotaExceededError
        // and other storage errors without crashing the React state update
        Eif (typeof window !== 'undefined') {
          try {
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
          } catch (storageError) {
            console.error(`Error saving to localStorage key "${key}":`, storageError);
          }
        }
        
        return valueToStore;
      });
    } catch (error) {
      // If error writing to localStorage, log but don't throw
      console.error(`Error saving to localStorage key "${key}":`, error);
    }
  }, [key]);
 
  // Sync with changes to localStorage from other tabs/windows
  useEffect(() => {
    Iif (typeof window === 'undefined') {
      return;
    }
 
    const handleStorageChange = (e: StorageEvent): void => {
      if (e.key === key && e.newValue !== null) {
        try {
          setStoredValue(JSON.parse(e.newValue));
        } catch (error) {
          console.warn(`Error parsing storage event for key "${key}":`, error);
        }
      }
    };
 
    // Listen for changes in other tabs/windows
    window.addEventListener('storage', handleStorageChange);
 
    return () => {
      window.removeEventListener('storage', handleStorageChange);
    };
  }, [key]);
 
  return [storedValue, setValue];
}