Problem
Need to persist UI state (theme, mode, preferences) across sessions without:- Adding database columns
- Making backend API calls
- Complicating component API
Solution: Context + useEffect + localStorage
This pattern is production-ready for any UI toggle, preference, or mode that should survive refresh/revisits.Core Pattern
Why This Shape?
- Single useEffect: Runs once, reads localStorage, sets
mounted. Prevents hydration mismatches. mountedFlag: Tells consumer when data is hydrated. Use it to avoid rendering before localStorage is read.- Immediate State + Async Sync:
setValue()is synchronous (updates immediately), localStorage write is fire-and-forget (no await). - Null Check in Hook: Catches misconfigured provider nesting at dev time.
Consumer Code
Testing
Wrap test component in provider:Common Use Cases
| Pattern | localStorage Key | Type | Example |
|---|---|---|---|
| Dark/Light Theme | appearance-theme | boolean | "light" | "dark" |
| Compact Mode | appearance-compact | boolean | "true" | "false" |
| Animations On/Off | appearance-animations | boolean | "true" | "false" |
| Language Preference | language | string | "en-US" | "es" |
| Sidebar Collapsed | ui-sidebar-collapsed | boolean | "true" | "false" |
Why NOT Redux/Zustand?
For simple boolean/string toggles that persist to localStorage:- No action creators needed: Context + hook is lighter
- No async middleware: localStorage is synchronous
- No normalized state: Single value is the whole store
- Fewer dependencies: React built-ins only
- Complex nested state structures
- Actions with side effects
- State shared across many routes
- Undo/redo capabilities
Gotchas
Hydration Mismatch
Problem: SSR renders without localStorage, client hydrates with different value. Fix: Always usemounted flag; don’t render until hydrated.
localStorage Called in Component Body
Problem:Forgetting Provider Wrapper
Problem: Component uses hook outside provider → throws “undefined context” error. Fix: Ensure provider wraps component in tree. Useroot/providers.tsx in Next.js projects.
Not Handling JSON Values
Problem: Storing arrays/objects as strings without serialization. Fix:Performance Notes
- localStorage reads/writes are synchronous (not async)
- Reading is ~1ms, writing is ~0.5ms on most browsers
- Safe to call on every toggle (no performance risk)
- If storing large objects (>1MB), consider IndexedDB instead
Across Codebases
This pattern appears in:meow-web(cat mode, animations)whiskers-landing(theme preference)- Any Next.js app with client-side state
- Mobile apps via AsyncStorage equivalent