Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions country-explorer/src/hooks/useFavorites.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);
// Lazily read any saved favorites on first render, so the app opens
// already showing what the user had saved before.
const [favoriteCodes, setFavoriteCodes] = useState<string[]>(() => 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.
Expand Down
29 changes: 29 additions & 0 deletions country-explorer/src/utils/localStorage.ts
Original file line number Diff line number Diff line change
@@ -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.
}
}