diff --git a/country-explorer/src/hooks/useFavorites.ts b/country-explorer/src/hooks/useFavorites.ts index 1064245..2b74e04 100644 --- a/country-explorer/src/hooks/useFavorites.ts +++ b/country-explorer/src/hooks/useFavorites.ts @@ -1,15 +1,47 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { readLocalValue, writeLocalValue } from '../utils/localStorage'; + +// sessionStorage/localStorage keys in this app are all namespaced by the +// wrapper in utils/localStorage.ts; this is just the per-value suffix. +const FAVORITES_STORAGE_KEY = 'favoriteCountryCodes'; + +// A stored value is only trusted if it parses as a JSON array of non-empty +// strings. Anything else — nothing saved yet, corrupted JSON, or a value +// that isn't an array of strings — is treated as "no favorites saved" so a +// broken or tampered-with value never crashes the app. +function readStoredFavorites(): string[] { + const stored = readLocalValue(FAVORITES_STORAGE_KEY); + if (!stored) return []; + + try { + const parsed: unknown = JSON.parse(stored); + if (!Array.isArray(parsed)) return []; + + return parsed.filter((code): code is string => typeof code === 'string' && code.length > 0); + } catch { + return []; + } +} /** * Tracks which countries the user has marked as favorites, identified by * their stable country code (the alpha-3 code already used as `Country.code` * everywhere else in the app). * - * This only holds the in-memory React state — persisting favorites to - * localStorage is handled separately. + * Only the country codes are persisted to localStorage — never the fetched + * country data itself — so favorites are retained after a reload and after + * the browser is closed and reopened. */ export function useFavorites() { - const [favoriteCodes, setFavoriteCodes] = useState([]); + // Lazily read any saved favorites on first render, so the app opens + // already showing what the user had saved before. + const [favoriteCodes, setFavoriteCodes] = useState(() => readStoredFavorites()); + + // Re-persist whenever the favorite list changes (add, remove, or the + // initial load being replaced by a correction). + useEffect(() => { + writeLocalValue(FAVORITES_STORAGE_KEY, JSON.stringify(favoriteCodes)); + }, [favoriteCodes]); const addFavorite = useCallback((code: string) => { // Guard against duplicates instead of relying on callers to check first. diff --git a/country-explorer/src/utils/localStorage.ts b/country-explorer/src/utils/localStorage.ts new file mode 100644 index 0000000..06569a0 --- /dev/null +++ b/country-explorer/src/utils/localStorage.ts @@ -0,0 +1,29 @@ +/** + * Thin, defensive wrapper around localStorage for small, user-created state + * that should persist across browser sessions (e.g. favorites) — never the + * fetched country data itself. Keys are namespaced so they don't collide + * with anything else on the page, and every call is wrapped in a try/catch + * since storage can be unavailable or full (e.g. private browsing, quota + * exceeded) — a failed read/write should never crash the app. + */ +const NAMESPACE = 'country-explorer'; + +function storageKey(key: string): string { + return `${NAMESPACE}:${key}`; +} + +export function readLocalValue(key: string): string | null { + try { + return window.localStorage.getItem(storageKey(key)); + } catch { + return null; + } +} + +export function writeLocalValue(key: string, value: string): void { + try { + window.localStorage.setItem(storageKey(key), value); + } catch { + // Storage unavailable or full — the choice just won't persist. + } +}