- Published on
Guide to use LocalStorage in Next js 14
In this guide, we'll explore the ins and outs of using LocalStorage in Next.js 14. Let's get started!
Table of Contents
What is LocalStorage?
Before we jump into the Next.js specifics, let's take a quick moment to understand what LocalStorage is all about.
LocalStorage is like a mini-fridge for your web app. It's a way to store key-value pairs in a user's browser. The cool thing? This data sticks around even after the browser window is closed.
Here are some key features of LocalStorage:
- It can store about 5-10MB of data (depending on the browser)
- The data is specific to the origin (domain) of the page
- The stored data is just strings, so you'll need to do some JSON magic for complex data
Using LocalStorage in Next.js 14
Now that we've got the basics down, let's see how we can use LocalStorage in our Next.js 14 apps.
The Client-Side Catch
Here's the thing: LocalStorage is a browser API. That means it only exists on the client-side. In Next.js, we need to be careful about when and where we use it.
Remember, Next.js does server-side rendering by default.
If you try to use LocalStorage in a component that's rendered on the server, you'll run into errors becuase of "hydration mismatch".
The useEffect Hook
To use LocalStorage safely in Next.js, we need to wrap it in a useEffect
hook. This ensures that the LocalStorage code only runs in the browser.
Here's a simple example:
import { useEffect, useState } from 'react';
const ExampleComponent = () => {
const [name, setName] = useState<string>('');
useEffect(() => {
// This code only runs in the browser
const storedName = localStorage.getItem('name');
if (storedName) {
setName(storedName);
}
}, []);
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newName = event.target.value;
setName(newName);
localStorage.setItem('name', newName);
};
return (
<div>
<input
type="text"
value={name}
onChange={handleNameChange}
placeholder="Enter your name"
/>
<p>Hello, {name}!</p>
</div>
);
};
export default ExampleComponent;
In this example, we're storing the user's name in LocalStorage. When the component mounts, we check if there's a stored name and update the state if there is. When the user types a new name, we update both the state and LocalStorage.
Creating a Custom Hook
To make our LocalStorage interactions even smoother, we can create a custom hook. This will help us reuse LocalStorage logic across our app.
Here's a simple useLocalStorage
hook:
import { useState, useEffect } from 'react'
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(initialValue)
useEffect(() => {
try {
const item = window.localStorage.getItem(key)
if (item) {
setStoredValue(JSON.parse(item))
}
} catch (error) {
console.log(error)
}
}, [key])
const setValue = (value: T) => {
try {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.log(error)
}
}
return [storedValue, setValue]
}
export default useLocalStorage
Now we can use this hook in our components like this:
const [name, setName] = useLocalStorage<string>('name', '')
Advanced LocalStorage Techniques
Now that we've got the basics down, let's look at some advanced techniques to level up your LocalStorage game.
Storing Complex Data
Remember how we said LocalStorage only stores strings? Well, that doesn't mean we can't store complex data. We just need to use JSON.stringify()
when setting items and JSON.parse()
when getting them.
// Storing an object
const user = { name: 'Alice', age: 30 }
localStorage.setItem('user', JSON.stringify(user))
// Retrieving the object
const storedUser = JSON.parse(localStorage.getItem('user') || '{}')
Handling LocalStorage Events
LocalStorage can also trigger events. This is super useful when you want different parts of your app to react to LocalStorage changes.
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'theme') {
// Do something when the theme changes
}
}
window.addEventListener('storage', handleStorageChange)
return () => {
window.removeEventListener('storage', handleStorageChange)
}
}, [])
LocalStorage and Server-Side Rendering
When working with Next.js, you might run into situations where you need to access LocalStorage data during server-side rendering. While you can't directly access LocalStorage on the server, you can pass the data as props from the client to the server.
Here's a pattern you might find useful:
import { useEffect, useState } from 'react'
const ServerRenderedComponent = ({ initialTheme }: { initialTheme: string }) => {
const [theme, setTheme] = useState(initialTheme)
useEffect(() => {
// This code only runs in the browser
const storedTheme = localStorage.getItem('theme')
if (storedTheme) {
setTheme(storedTheme)
}
}, [])
// Rest of your component logic
}
export async function getServerSideProps() {
// You can't access localStorage here, so we pass a default value
return {
props: { initialTheme: 'light' },
}
}
export default ServerRenderedComponent
Performance Considerations
While LocalStorage is super handy, it's important to use it wisely to keep your Next.js app running smoothly.
Avoid Overuse
LocalStorage has limited space (usually around 5MB). Don't treat it like a database! Use it for small amounts of data that genuinely need to persist.
Batch Operations
If you're doing a lot of read/write operations with LocalStorage, consider batching them. Instead of updating individual items, update a single object:
// Instead of this:
localStorage.setItem('name', 'Alice')
localStorage.setItem('age', '30')
localStorage.setItem('city', 'Wonderland')
// Do this:
const userData = {
name: 'Alice',
age: '30',
city: 'Wonderland',
}
localStorage.setItem('userData', JSON.stringify(userData))
Troubleshooting Common LocalStorage Issues
Even with careful implementation, you might run into some hiccups when working with LocalStorage in Next.js. Here are a few common issues and their solutions:
1. "localStorage is not defined" Error
This usually happens when you're trying to access LocalStorage during server-side rendering. Wrap your LocalStorage calls in a check:
if (typeof window !== 'undefined') {
// LocalStorage code here
}
2. Data Not Persisting
If your data isn't sticking around, make sure you're not accidentally clearing it somewhere else in your code. Also, check if you're in incognito/private browsing mode, which might prevent data persistence.
3. Unexpected Data Types
Remember, LocalStorage stores everything as strings. If you're getting unexpected data types, make sure you're parsing your data correctly when retrieving it:
const storedValue = JSON.parse(localStorage.getItem('myData') || '{}')
4. Quota Exceeded Error
If you're hitting storage limits, consider cleaning up old or unnecessary data, or look into using IndexedDB for larger storage needs.
Error Handling For Quota Errors
When you hit these errors, you've got a few options:
Catch and Handle
Wrap your LocalStorage operations in a try-catch block:
try {
localStorage.setItem('myKey', 'myValue')
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
console.log("Storage full, let's make some room!")
// Handle the error (see strategies below)
}
}
Remove old items
const removeOldestItem = () => {
if (localStorage.length > 0) {
const oldestKey = Object.keys(localStorage)[0]
localStorage.removeItem(oldestKey)
}
}
Implement an expiration system
const setItemWithExpiry = (key: string, value: string, ttl: number) => {
const now = new Date()
const item = {
value: value,
expiry: now.getTime() + ttl,
}
localStorage.setItem(key, JSON.stringify(item))
}
const getItemWithExpiry = (key: string) => {
const itemStr = localStorage.getItem(key)
if (!itemStr) return null
const item = JSON.parse(itemStr)
const now = new Date()
if (now.getTime() > item.expiry) {
localStorage.removeItem(key)
return null
}
return item.value
}
Use Compression
If you're storing large strings, consider compressing them:
import { compress, decompress } from 'lz-string'
const setCompressedItem = (key: string, value: string) => {
const compressed = compress(value)
localStorage.setItem(key, compressed)
}
const getCompressedItem = (key: string) => {
const compressed = localStorage.getItem(key)
return compressed ? decompress(compressed) : null
}
Consider Alternatives
If you're consistently hitting limits, it might be time to look at other options:
- IndexedDB: Offers more storage space and better performance for large amounts of structured data.
- Server-side storage: For data that needs to persist across devices or sessions.
Conclusion
Remember, LocalStorage is a powerful tool, but it's not a replacement for a proper database. Use it for small amounts of data that need to persist across page reloads or browser sessions.