
LocalStorage Not Working in Next.js App Router: Complete Guide & Solutions
Introduction
Are you struggling with localStorage not working in Next.js App Router? You're not alone. Many developers encounter this issue when building applications with Next.js 13+ using the App Router. The problem is simple but often confusing: localStorage is undefined in Next.js App Router because of server-side rendering.
In this comprehensive guide, I'll explain why localStorage fails in Next.js SSR environments and show you multiple solutions to fix it. By the end, you'll understand how to properly implement client-side storage in Next.js App Router without errors.
Why LocalStorage Stops Working in Next.js App Router
Before jumping into solutions, let's understand why localStorage is not available in Next.js App Router.
The Root Cause: Server-Side Rendering (SSR)
Next.js App Router components are server components by default. They run on the server before being sent to the browser. localStorage is a browser API that only exists on the client side. When your server tries to access localStorage, it gets an error because the object doesn't exist in the Node.js environment.
Here's what happens:
- Server renders the component - localStorage doesn't exist in Node.js
- Error thrown:
ReferenceError: localStorage is not defined - Component fails to hydrate properly on the client
- User sees a blank page or console errors
This is the core reason why localStorage not working Next.js frustrates developers new to SSR environments.
The Problem Explained: Common Errors
Error 1: Direct localStorage Access
// ❌ This will FAIL in Next.js App Router
export default function Component() {
const data = localStorage.getItem('key'); // ERROR on server!
return <div>{data}</div>;
}Error message: ReferenceError: localStorage is not defined
Error 2: Inside useEffect (Still Problematic Without 'use client')
// ❌ Still fails without 'use client' directive
import { useEffect, useState } from 'react';
export default function Component() {
const [data, setData] = useState(null);
useEffect(() => {
const stored = localStorage.getItem('key');
setData(stored);
}, []);
return <div>{data}</div>;
}Prerequisites
To follow this guide on how to fix localStorage in Next.js, you'll need:
- Next.js 13+ with App Router configured
- Basic understanding of React hooks
- Knowledge of server vs. client components
- Node.js and npm installed
Solution 1: Use the 'use client' Directive (Recommended)
The simplest way to fix localStorage not working in Next.js App Router is to mark the component as a client component using the 'use client' directive.
How It Works
When you add 'use client' at the top of a file, Next.js treats it as a client-side component. This prevents server-side rendering and lets localStorage work normally.
Implementation
Create a new file: components/ClientComponent.jsx
'use client';
import { useEffect, useState } from 'react';
export default function ClientComponent() {
const [data, setData] = useState(null);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
// Set mounted flag to prevent hydration mismatch
setIsMounted(true);
// Now localStorage is safe to use
const stored = localStorage.getItem('userPreference');
setData(stored);
}, []);
// Prevent rendering until component is mounted on client
if (!isMounted) {
return <div>Loading...</div>;
}
return (
<div>
<p>Stored data: {data}</p>
</div>
);
}Why This Works
'use client'directive moves rendering to the browseruseEffectonly runs on the client after hydrationisMountedcheck prevents hydration mismatches
Solution 2: Create a Custom useLocalStorage Hook
For reusable localStorage functionality in Next.js App Router, create a custom hook that handles all edge cases.
The useLocalStorage Hook
Create hooks/useLocalStorage.js:
'use client';
import { useEffect, useState } from 'react';
export function useLocalStorage(key, initialValue) {
// State to store our value
const [storedValue, setStoredValue] = useState(initialValue);
// State to track if component is mounted (prevents hydration mismatch)
const [isMounted, setIsMounted] = useState(false);
// Initialize on client mount
useEffect(() => {
setIsMounted(true);
try {
// Check if localStorage is available
if (typeof window === 'undefined') {
return;
}
// Get stored value from localStorage
const item = window.localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
}
}, [key]);
// Function to set value in localStorage
const setValue = (value) => {
try {
if (typeof window === 'undefined') {
console.warn('localStorage is not available on server');
return;
}
// Allow value to be a function for same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
// Don't render until mounted to avoid hydration mismatch
return isMounted ? [storedValue, setValue] : [initialValue, setValue];
}Usage Example
Create components/UserPreferences.jsx:
'use client';
import { useLocalStorage } from '@/hooks/useLocalStorage';
export function UserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'en');
const handleThemeChange = (newTheme) => {
setTheme(newTheme);
};
return (
<div>
<h2>Preferences</h2>
<div>
<label htmlFor="theme">Theme: </label>
<select
id="theme"
value={theme}
onChange={(e) => handleThemeChange(e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto</option>
</select>
</div>
<div>
<label htmlFor="language">Language: </label>
<select
id="language"
value={language}
onChange={(e) => setLanguage(e.target.value)}
>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
</div>
<p>Current Theme: {theme}</p>
<p>Current Language: {language}</p>
</div>
);
}Why This Hook is Better
- ✅ Handles localStorage not working in SSR automatically
- ✅ Prevents hydration mismatch in Next.js
- ✅ Checks if
windowexists before accessinglocalStorage - ✅ Has error handling and logging
- ✅ Same API as
useStatefor familiarity
Solution 3: Check if Window Is Defined
For localStorage access in Next.js, always check if window is defined before using it.
Safe localStorage Access Pattern
'use client';
import { useEffect, useState } from 'react';
export function SafeStorageComponent() {
const [isLoaded, setIsLoaded] = useState(false);
const [savedValue, setSavedValue] = useState('');
useEffect(() => {
// Ensure we're on the client side
if (typeof window !== 'undefined') {
const value = localStorage.getItem('myKey') || '';
setSavedValue(value);
setIsLoaded(true);
}
}, []);
const handleSave = () => {
if (typeof window !== 'undefined') {
localStorage.setItem('myKey', 'new value');
setSavedValue('new value');
}
};
if (!isLoaded) {
return <p>Loading preferences...</p>;
}
return (
<div>
<p>Saved Value: {savedValue}</p>
<button onClick={handleSave}>Save to localStorage</button>
</div>
);
}Key Points
typeof window !== 'undefined'checks if code runs on client- This pattern prevents localStorage is not defined Next.js errors
- Safe for server-side rendering environments
Solution 4: Wrap localStorage in a Context Provider
For managing localStorage across Next.js App Router, use React Context for global state management.
Create a Storage Context
Create context/StorageContext.jsx:
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
const StorageContext = createContext();
export function StorageProvider({ children }) {
const [isClient, setIsClient] = useState(false);
const [storage, setStorage] = useState({});
useEffect(() => {
setIsClient(true);
// Load all stored data on mount
if (typeof window !== 'undefined') {
const allData = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
allData[key] = localStorage.getItem(key);
}
setStorage(allData);
}
}, []);
const getItem = (key) => {
if (!isClient) return null;
return typeof window !== 'undefined' ? localStorage.getItem(key) : null;
};
const setItem = (key, value) => {
if (!isClient || typeof window === 'undefined') return;
localStorage.setItem(key, value);
setStorage(prev => ({ ...prev, [key]: value }));
};
const removeItem = (key) => {
if (!isClient || typeof window === 'undefined') return;
localStorage.removeItem(key);
setStorage(prev => {
const newStorage = { ...prev };
delete newStorage[key];
return newStorage;
});
};
const clear = () => {
if (!isClient || typeof window === 'undefined') return;
localStorage.clear();
setStorage({});
};
return (
<StorageContext.Provider value={{ getItem, setItem, removeItem, clear, isClient }}>
{children}
</StorageContext.Provider>
);
}
export function useStorage() {
const context = useContext(StorageContext);
if (!context) {
throw new Error('useStorage must be used within StorageProvider');
}
return context;
}Use the Context in Layout
Update app/layout.jsx:
import { StorageProvider } from '@/context/StorageContext';
export default function RootLayout({ children }) {
return (
<html>
<body>
<StorageProvider>
{children}
</StorageProvider>
</body>
</html>
);
}Use in Components
'use client';
import { useStorage } from '@/context/StorageContext';
export function MyComponent() {
const { getItem, setItem } = useStorage();
const handleSave = () => {
setItem('userName', 'John Doe');
};
const handleLoad = () => {
const name = getItem('userName');
console.log('Loaded name:', name);
};
return (
<div>
<button onClick={handleSave}>Save User Name</button>
<button onClick={handleLoad}>Load User Name</button>
</div>
);
}Solution 5: Use IndexedDB or SessionStorage for Large Data
When localStorage limitations in Next.js become a problem, consider alternatives for large data storage in Next.js.
Using SessionStorage
'use client';
import { useEffect, useState } from 'react';
export function SessionStorageExample() {
const [data, setData] = useState(null);
useEffect(() => {
if (typeof window !== 'undefined') {
// Use sessionStorage (cleared when tab closes)
const stored = sessionStorage.getItem('tempData');
setData(stored);
}
}, []);
const handleSave = () => {
if (typeof window !== 'undefined') {
sessionStorage.setItem('tempData', 'temporary value');
setData('temporary value');
}
};
return (
<div>
<p>Session Data: {data}</p>
<button onClick={handleSave}>Save to SessionStorage</button>
</div>
);
}Using IndexedDB for Complex Data
'use client';
import { useEffect, useState } from 'react';
export function IndexedDBExample() {
const [db, setDb] = useState(null);
useEffect(() => {
if (typeof window === 'undefined') return;
const request = indexedDB.open('MyDatabase', 1);
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains('items')) {
database.createObjectStore('items', { keyPath: 'id' });
}
};
request.onsuccess = () => {
setDb(request.result);
};
request.onerror = () => {
console.error('IndexedDB opening failed');
};
}, []);
const saveData = (item) => {
if (!db) return;
const transaction = db.transaction(['items'], 'readwrite');
const store = transaction.objectStore('items');
store.add(item);
};
return (
<div>
<p>IndexedDB connection: {db ? 'Connected' : 'Disconnected'}</p>
<button onClick={() => saveData({ id: 1, name: 'Item 1' })}>
Save to IndexedDB
</button>
</div>
);
}Troubleshooting: Common Issues & Fixes
Issue 1: Hydration Mismatch Error
Error: Hydration failed because the initial UI does not match what was rendered on the server
Fix: Always use the isMounted pattern:
'use client';
import { useEffect, useState } from 'react';
export function Component() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) return null;
return <div>{localStorage.getItem('key')}</div>;
}Issue 2: localStorage is undefined
Error: Cannot read properties of undefined (reading 'getItem')
Fix: Check if window exists:
if (typeof window !== 'undefined') {
const value = localStorage.getItem('key');
}Issue 3: Values Not Persisting
Problem: localStorage not persisting data in Next.js App Router
Fix: Ensure 'use client' directive is present and check browser's localStorage settings:
'use client';
useEffect(() => {
try {
localStorage.setItem('test', 'value');
const retrieved = localStorage.getItem('test');
console.log('localStorage working:', retrieved === 'value');
} catch (e) {
console.error('localStorage error:', e);
}
}, []);Issue 4: Private/Incognito Mode
Problem: localStorage not available in private mode browsers
Solution: Implement graceful fallback:
'use client';
function canUseLocalStorage() {
try {
const test = '__test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch {
return false;
}
}
export function SafeComponent() {
const [data, setData] = useState(null);
useEffect(() => {
if (typeof window !== 'undefined' && canUseLocalStorage()) {
setData(localStorage.getItem('key'));
}
}, []);
return <div>{data || 'Storage not available'}</div>;
}Best Practices for localStorage in Next.js App Router
1. Always Use 'use client' Directive
'use client'; // Required for browser APIs
import { useEffect, useState } from 'react';2. Check Window Before Accessing localStorage
if (typeof window !== 'undefined') {
localStorage.getItem('key');
}3. Prevent Hydration Mismatches
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) return null;4. Use Custom Hooks for Reusability
// Encapsulate localStorage logic
export function useLocalStorage(key, initialValue) {
// ... hook implementation
}5. Add Error Handling
try {
localStorage.setItem(key, value);
} catch (error) {
console.error('localStorage error:', error);
}6. Serialize Complex Data
// Always use JSON for complex objects
localStorage.setItem('user', JSON.stringify(userData));
const user = JSON.parse(localStorage.getItem('user'));Performance Tips: Optimizing localStorage Usage
Debounce Writes to localStorage
'use client';
import { useEffect, useState, useRef } from 'react';
export function DebouncedStorageComponent() {
const [value, setValue] = useState('');
const timeoutRef = useRef(null);
useEffect(() => {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('value', value);
}
}, 500); // Wait 500ms before writing
return () => clearTimeout(timeoutRef.current);
}, [value]);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}Cache localStorage Values in State
'use client';
import { useEffect, useState } from 'react';
export function CachedStorageComponent() {
const [cachedValue, setCachedValue] = useState(null);
useEffect(() => {
if (typeof window !== 'undefined') {
// Read once and cache
const value = localStorage.getItem('key');
setCachedValue(value);
}
}, []);
// Use cachedValue instead of repeatedly reading localStorage
return <div>{cachedValue}</div>;
}SEO & Accessibility Considerations
When implementing localStorage in Next.js App Router, consider:
- SSR-first design: Don't rely on localStorage for critical page content
- Progressive enhancement: Ensure core functionality works without localStorage
- Fallback UI: Show loading states while localStorage loads
- Error messages: Provide clear feedback if storage isn't available
- Keyboard navigation: Ensure preference controls are keyboard accessible
'use client';
export function AccessiblePreferences() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
// Show default content during SSR and hydration
if (!isMounted) {
return (
<div role="status" aria-live="polite">
<p>Loading your preferences...</p>
</div>
);
}
return (
<div role="region" aria-label="User Preferences">
{/* Component content */}
</div>
);
}Comparison: Solutions at a Glance
| Solution | Best For | Complexity | SSR Safe | |----------|----------|-----------|----------| | 'use client' directive | Simple components | Low | ✅ Yes | | useLocalStorage hook | Reusable logic | Medium | ✅ Yes | | typeof window check | Conditional access | Low | ✅ Yes | | Context Provider | Global state | High | ✅ Yes | | SessionStorage/IndexedDB | Large data | Medium | ✅ Yes |
Conclusion
LocalStorage not working in Next.js App Router is a common issue with a simple solution. By understanding that Next.js renders server components by default, and using the 'use client' directive combined with proper checking patterns, you can fix this problem completely.
The key takeaways for using localStorage in Next.js App Router are:
- Add
'use client'directive to components using browser APIs - Always check
typeof window !== 'undefined'before accessing localStorage - Use the
isMountedpattern to prevent hydration mismatches - Create custom hooks for reusable localStorage logic
- Implement proper error handling and fallbacks
Start with the simple 'use client' solution, and graduate to custom hooks as your application grows. Your users will thank you for the persistent preferences and improved experience!

Malik Saqib
I craft short, practical AI & web dev articles. Follow me on LinkedIn.