DEV Community

Cover image for IndexedDB vs localStorage: When to Use Which? πŸ—„οΈ
Oghenetega Adiri
Oghenetega Adiri

Posted on

IndexedDB vs localStorage: When to Use Which? πŸ—„οΈ

Hey fellow developers! Ever found yourself wondering whether to use localStorage or IndexedDB for your web app's data storage needs? You're not alone! Let's dive into both options and figure out when each one shines. I promise to keep this practical and skip the boring parts. πŸ˜‰

The TL;DR Version

  • localStorage: Simple, synchronous, perfect for small data (strings only)
  • IndexedDB: Complex, asynchronous, handles large data like a boss

Now let's get into the good stuff!

localStorage: The Simple One πŸ“¦

localStorage is like that reliable friend who's always there when you need them - straightforward, no-frills, gets the job done.

Quick Facts:

  • Stores key-value pairs as strings
  • Synchronous API (blocks your code)
  • ~5-10MB storage limit (browser dependent)
  • Data persists until manually cleared

How to Use localStorage

// Storing data
localStorage.setItem('username', 'adiriTega');
localStorage.setItem('theme', 'dark');

// Storing objects (need to stringify first!)
const userSettings = {
  theme: 'dark',
  notifications: true,
  language: 'en'
};
localStorage.setItem('settings', JSON.stringify(userSettings));

// Retrieving data
const username = localStorage.getItem('username');
const settings = JSON.parse(localStorage.getItem('settings'));

// Removing items
localStorage.removeItem('username');

// Clear everything (nuclear option!)
localStorage.clear();
Enter fullscreen mode Exit fullscreen mode

LocalStorage Pro Tips πŸ’‘

  1. Always handle errors:
try {
  localStorage.setItem('key', 'value');
} catch (error) {
  console.error('Storage failed:', error);
  // Maybe the user is in private mode or storage is full
}
Enter fullscreen mode Exit fullscreen mode
  1. Create a wrapper for objects:
const storage = {
  set: (key, value) => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (error) {
      console.error('Failed to save:', error);
      return false;
    }
  },

  get: (key, defaultValue = null) => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    } catch (error) {
      console.error('Failed to get:', error);
      return defaultValue;
    }
  }
};

// Usage
storage.set('user', { name: 'John', age: 30 });
const user = storage.get('user', {});
Enter fullscreen mode Exit fullscreen mode

IndexedDB: The Powerhouse πŸ’ͺ

IndexedDB is like that overachiever friend who can handle anything you throw at them. More complex? Sure. More powerful? Absolutely!

Quick Facts:

  • Stores any JavaScript data type
  • Asynchronous API (non-blocking)
  • Much larger storage capacity (often hundreds of MB)
  • Supports transactions, indexes, and queries

How to Use IndexedDB

Let's build a simple task manager to see IndexedDB in action:

// Initialize database
function initDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('TaskManager', 1);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;

      // Create object store
      const store = db.createObjectStore('tasks', { 
        keyPath: 'id', 
        autoIncrement: true 
      });

      // Create indexes
      store.createIndex('status', 'status', { unique: false });
      store.createIndex('priority', 'priority', { unique: false });
    };
  });
}

// Add a task
async function addTask(task) {
  const db = await initDB();

  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['tasks'], 'readwrite');
    const store = transaction.objectStore('tasks');
    const request = store.add(task);

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Get all tasks
async function getAllTasks() {
  const db = await initDB();

  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['tasks'], 'readonly');
    const store = transaction.objectStore('tasks');
    const request = store.getAll();

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// Usage
async function demo() {
  try {
    // Add a new task
    await addTask({
      title: 'Learn IndexedDB',
      status: 'pending',
      priority: 'high',
      dueDate: new Date('2025-06-01')
    });

    // Get all tasks
    const tasks = await getAllTasks();
    console.log('All tasks:', tasks);
  } catch (error) {
    console.error('Error:', error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Modern IndexedDB with Promises

The callback-heavy API can be quite painful to implement. Here's a cleaner approach:

class TaskDB {
  constructor() {
    this.dbPromise = null;
  }

  async getDB() {
    if (!this.dbPromise) {
      this.dbPromise = this.openDB();
    }
    return this.dbPromise;
  }

  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('TaskManager', 1);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains('tasks')) {
          const store = db.createObjectStore('tasks', { 
            keyPath: 'id', 
            autoIncrement: true 
          });
          store.createIndex('status', 'status');
        }
      };
    });
  }

  async add(task) {
    const db = await this.getDB();
    return new Promise((resolve, reject) => {
      const transaction = db.transaction(['tasks'], 'readwrite');
      const store = transaction.objectStore('tasks');
      const request = store.add(task);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async getByStatus(status) {
    const db = await this.getDB();
    return new Promise((resolve, reject) => {
      const transaction = db.transaction(['tasks'], 'readonly');
      const store = transaction.objectStore('tasks');
      const index = store.index('status');
      const request = index.getAll(status);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

// Usage
const taskDB = new TaskDB();
taskDB.add({ title: 'Build awesome app', status: 'pending' });
Enter fullscreen mode Exit fullscreen mode

The Ultimate Comparison πŸ₯Š

Feature localStorage IndexedDB
Simplicity 🟒 Super simple πŸ”΄ More complex
Performance πŸ”΄ Synchronous (blocking) 🟒 Asynchronous
Storage Capacity πŸ”΄ 5-10MB typical 🟒 Much larger
Data Types πŸ”΄ Strings only 🟒 Any JS type
Querying πŸ”΄ Key-only 🟒 Indexes & queries
Transactions πŸ”΄ No 🟒 Yes
Browser Support 🟒 Excellent 🟒 Good (not IE)

When to Use What? πŸ€”

Use localStorage when:

  • Storing simple key-value pairs
  • Data size is small (< 5MB)
  • You need quick, synchronous access
  • Storing user preferences, tokens, or simple settings

Use IndexedDB when:

  • Storing complex data structures
  • Working with large datasets
  • Need offline data storage
  • Building complex queries
  • Creating offline-first applications

Real-World Examples

localStorage: User Preferences

const preferences = {
  saveUserPreferences(prefs) {
    localStorage.setItem('userPrefs', JSON.stringify(prefs));
  },

  getUserPreferences() {
    const prefs = localStorage.getItem('userPrefs');
    return prefs ? JSON.parse(prefs) : {
      theme: 'light',
      language: 'en',
      notifications: true
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

IndexedDB: Offline-First Todo App

class OfflineTodoApp {
  constructor() {
    this.dbPromise = this.initDB();
  }

  async initDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('TodoApp', 1);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        const store = db.createObjectStore('todos', { 
          keyPath: 'id',
          autoIncrement: true 
        });

        store.createIndex('completed', 'completed');
        store.createIndex('category', 'category');
      };

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async addTodo(todo) {
    const db = await this.dbPromise;
    const transaction = db.transaction(['todos'], 'readwrite');
    const store = transaction.objectStore('todos');

    return new Promise((resolve, reject) => {
      const request = store.add({
        ...todo,
        createdAt: Date.now(),
        synced: false // For later syncing with server
      });

      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async toggleComplete(id) {
    const db = await this.dbPromise;
    const transaction = db.transaction(['todos'], 'readwrite');
    const store = transaction.objectStore('todos');

    const todo = await new Promise((resolve) => {
      const request = store.get(id);
      request.onsuccess = () => resolve(request.result);
    });

    if (todo) {
      todo.completed = !todo.completed;
      todo.synced = false;

      return new Promise((resolve, reject) => {
        const request = store.put(todo);
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Pro Tips for Both

  1. Always handle quota exceeded errors:
try {
  // Your storage code
} catch (error) {
  if (error.name === 'QuotaExceededError') {
    // Handle storage full scenario
    alert('Storage is full! Please clear some data.');
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Implement fallbacks:
const storage = {
  isAvailable() {
    try {
      const test = '__test__';
      localStorage.setItem(test, test);
      localStorage.removeItem(test);
      return true;
    } catch (e) {
      return false;
    }
  },

  set(key, value) {
    if (this.isAvailable()) {
      localStorage.setItem(key, JSON.stringify(value));
    } else {
      // Fallback to cookies or send to server
    }
  }
};
Enter fullscreen mode Exit fullscreen mode
  1. Consider using libraries:

Conclusion πŸŽ‰

Both localStorage and IndexedDB have their place in modern web development. localStorage is perfect for simple data storage, while IndexedDB shines when you need power and flexibility.

The key is understanding your needs:

  • Small, simple data? Go with localStorage.
  • Complex, large-scale data? IndexedDB is your friend.

Remember, the best storage solution is the one that fits your specific use case. Don't use a sledgehammer to kill an ant!

Happy coding.


What's your experience with browser storage? Drop a comment below and share your favorite tips and tricks

Top comments (0)