Problem Statement
Build a counter whose value survives a page reload by saving it in localStorage, then pull that logic into a reusable hook.
Functional Requirements
- Counter: Show a number and an Increment button that adds 1.
- Persist: Save the count under the
countkey inlocalStorageevery time it changes. - Restore: On load, read the saved count. If nothing is saved, start at 0.
- Reusable hook: Expose
usePersistedState(key, initialValue)that returns[value, setValue]likeuseState.
Technical Constraints
- Store values as JSON with
JSON.stringifyand read them back withJSON.parse. - Wrap
localStoragecalls intry/catchso a quota error or bad JSON does not crash the app. - Use TypeScript generics so the hook works with any value type.
What is localStorage?
localStorage is a web API that allows you to:
- Store key-value pairs in the browser
- Data persists even after the browser is closed
- Limited to storing strings only (typically 5-10 MB)
- Data remains until explicitly cleared
// Basic localStorage Usage
localStorage.setItem("name", "John"); // Stores data
localStorage.getItem("name"); // Retrieves data
localStorage.removeItem("name"); // Removes data
localStorage.clear(); // Clears all dataDeep Dive into Both Approaches
Approach 1: Direct Implementation
1. The Storage Utility Functions
// setItem function breakdown
export function setItem(key: string, value: unknown) {
try {
// 1. Converts any JavaScript value to a string
// 2. Stores in localStorage with the given key
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}
// getItem function breakdown
export function getItem(key: string) {
try {
// 1. Retrieves string from localStorage
const item = window.localStorage.getItem(key);
// 2. Converts string back to JavaScript value
// 3. Returns undefined if item doesn't exist
return item ? JSON.parse(item) : undefined;
} catch (error) {
console.error(error);
}
}2. Component Implementation
function App() {
// Lazy initialization with stored value
const [count, setCount] = useState(() => {
const item = getItem("count"); // Check localStorage first
return (item as number) || 0; // Fallback to 0 if not found
});
// Sync with localStorage whenever count changes
useEffect(() => {
setItem("count", count);
}, [count]);
}Approach 2: Custom Hook Implementation
export function usePersistedState<T>(key: string, initialValue: T) {
// Generic type T allows hook to work with any data type
const [value, setValue] = useState(() => {
const item = getItem(key);
// Check for undefined, not falsy, so a stored 0, false or "" is kept
return item !== undefined ? (item as T) : initialValue;
});
// Automatically sync with localStorage
useEffect(() => {
setItem(key, value);
}, [key, value]);
return [value, setValue] as const;
}Core Concepts to Remember Forever
-
JSON Handling
// Why we need JSON methods: JSON.stringify(value); // Converts objects/arrays to strings JSON.parse(string); // Converts strings back to objects/arrays -
Error Handling
try { // localStorage operations can fail due to: // - Storage quota exceeded // - Private browsing mode // - Invalid JSON } catch (error) { console.error(error); } -
Lazy Initialization
// Instead of: const [state] = useState(expensiveOperation()); // Use: const [state] = useState(() => expensiveOperation()); -
TypeScript Benefits
// Generic type ensures type safety function usePersistedState<T>(key: string, initialValue: T); // Usage: const [count, setCount] = usePersistedState<number>("count", 0); const [user, setUser] = usePersistedState<User>("user", defaultUser);
Key Differences Table
| Feature | Approach 1 (Direct) | Approach 2 (Hook) |
|---|---|---|
| Reusability | Limited to one use case | Highly reusable |
| Code Organization | More code in component | Encapsulated in hook |
| Type Safety | Basic | Advanced with generics |
| Implementation | Straightforward | More sophisticated |
When to Use Each Approach
Use Approach 1 When:
- Simple one-off storage needs
- Learning/understanding the concept
- Quick prototyping
Use Approach 2 When:
- Multiple components need persistence
- Working with different data types
- Building a production application
- Need reusable storage logic