Logo
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:

  1. IndexedDB: Offers more storage space and better performance for large amounts of structured data.
  2. 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.