# Browser Storage: LocalStorage SessionStorage IndexedDB

# Browser Storage: LocalStorage, SessionStorage, IndexedDB

## LocalStorage

### Problem
You need to persist user data across browser sessions without server involvement, such as user preferences, theme settings, or form data that should survive page refreshes and browser restarts.

### Solution
LocalStorage provides synchronous, persistent key-value storage with no expiration. Data remains until explicitly deleted or the browser cache is cleared.

### Code

```javascript
// Store data
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'John' }));

// Retrieve data
const theme = localStorage.getItem('theme');
const user = JSON.parse(localStorage.getItem('user'));

// Check if key exists
if (localStorage.getItem('theme')) {
  console.log('Theme preference found');
}

// Remove specific item
localStorage.removeItem('theme');

// Clear all storage
localStorage.clear();

// Get all keys
for (let i = 0; i < localStorage.length; i++) {
  const key = localStorage.key(i);
  console.log(`${key}: ${localStorage.getItem(key)}`);
}

// Listen for storage changes (from other tabs)
window.addEventListener('storage', (event) => {
  if (event.key === 'theme') {
    console.log(`Theme changed from ${event.oldValue} to ${event.newValue}`);
  }
});
```

### Tips
- **Limit**: ~5-10MB per domain (varies by browser)
- **Synchronous**: Blocks execution; avoid storing large objects
- **No expiration**: Manually manage cleanup
- **Cross-tab communication**: Storage events fire in other tabs, not current one
- **Security**: Never store sensitive data (passwords, tokens); vulnerable to XSS
- **Best for**: User preferences, theme settings, non-sensitive user data

---

## SessionStorage

### Problem
You need temporary storage that persists only during the current browser tab session, useful for multi-step forms, temporary UI state, or data that shouldn't survive a page close.

### Solution
SessionStorage works identically to LocalStorage but clears when the tab closes. Each tab has its own isolated storage.

### Code

```javascript
// Store form step data
sessionStorage.setItem('formStep', '2');
sessionStorage.setItem('formData', JSON.stringify({
  name: 'Alice',
  email: 'alice@example.com'
}));

// Retrieve during same session
const step = sessionStorage.getItem('formStep');
const data = JSON.parse(sessionStorage.getItem('formData'));

// Update multi-step form
function saveFormStep(stepNumber, data) {
  sessionStorage.setItem(`step_${stepNumber}`, JSON.stringify(data));
}

function getFormStep(stepNumber) {
  return JSON.parse(sessionStorage.getItem(`step_${stepNumber}`));
}

// Clear on form completion
function completeForm() {
  sessionStorage.removeItem('formStep');
  sessionStorage.removeItem('formData');
}

// Detect session end
window.addEventListener('beforeunload', () => {
  console.log('Session ending, data will be cleared');
});
```

### Tips
- **Isolation**: Each tab/window has separate storage; no cross-tab access
- **Auto-cleanup**: Automatically cleared when tab closes
- **Same limits**: ~5-10MB per domain
- **Synchronous**: Same performance considerations as LocalStorage
- **Best for**: Multi-step wizards, temporary UI state, session-specific data, draft content

---

## IndexedDB

### Problem
You need to store large amounts of structured data (megabytes to gigabytes), perform complex queries, create indexes, or work with objects without serialization. Examples: offline-first apps, caching API responses, managing large datasets.

### Solution
IndexedDB is an asynchronous, NoSQL database in the browser supporting transactions, indexes, and complex queries. It's the most powerful client-side storage option.

### Code

```javascript
// Open/create database
const dbRequest = indexedDB.open('myAppDB', 1);

dbRequest.onerror = () => console.error('DB open failed');

dbRequest.onsuccess = (event) => {
  const db = event.target.result;
  console.log('Database opened');
};

// Create object store (on version upgrade)
dbRequest.onupgradeneeded = (event) => {
  const db = event.target.result;
  
  // Create object store with primary key
  if (!db.objectStoreNames.contains('users')) {
    const userStore = db.createObjectStore('users', { keyPath: 'id' });
    
    // Create indexes
    userStore.createIndex('email', 'email', { unique: true });
    userStore.createIndex('name', 'name', { unique: false });
  }
};

// Add/update data
function addUser(db, user) {
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  const request = store.add(user); // or put() to overwrite

  request.onsuccess = () => console.log('User added');
  request.onerror = () => console.error('Add failed');
}

// Retrieve by primary key
function getUser(db, id) {
  const transaction = db.transaction(['users'], 'readonly');
  const store = transaction.objectStore('users');
  const request = store.get(id);

  request.onsuccess = () => {
    console.log('User:', request.result);
  };
}

// Query by index
function getUserByEmail(db, email) {
  const transaction = db.transaction(['users'], 'readonly');
  const store = transaction.objectStore('users');
  const index = store.index('email');
  const request = index.get(email);

  request.onsuccess = () => {
    console.log('Found user:', request.result);
  };
}

// Get all records
function getAllUsers(db) {
  const transaction = db.transaction(['users'], 'readonly');
  const store = transaction.objectStore('users');
  const request = store.getAll();

  request.onsuccess = () => {
    console.log('All users:', request.result);
  };
}

// Range query
function getUsersInRange(db, minId, maxId) {
  const transaction = db.transaction(['users'], 'readonly');
  const store = transaction.objectStore('users');
  const range = IDBKeyRange.bound(minId, maxId);
  const request = store.getAll(range);

  request.onsuccess = () => {
    console.log('Users in range:', request.result);
  };
}

// Delete record
function deleteUser(db, id) {
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  const request = store.delete(id);

  request.onsuccess = () => console.log('User deleted');
}

// Clear entire store
function clearUsers(db) {
  const transaction = db.transaction(['users'], 'readwrite');
  const store = transaction.objectStore('users');
  const request = store.clear();

  request.onsuccess = () => console.log('Store cleared');
}

// Promise wrapper (modern approach)
function openDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('myAppDB', 1);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

async function addUserAsync(user) {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['users'], 'readwrite');
    const request = transaction.objectStore('users').add(user);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}
```

### Tips
- **Asynchronous**: Non-blocking; use callbacks or Promises
- **Limit**: Typically 50MB+ (varies by browser; can request more)
- **Transactions**: Atomic operations; all-or-nothing
- **Indexes**: Enable fast queries without scanning all records
- **Cursor iteration**: Efficiently loop through large datasets
- **Best for**: Offline-first apps, large datasets, complex queries, caching

---

## Comparison Table

| Feature | LocalStorage | SessionStorage | IndexedDB |
|---------|--------------|----------------|-----------|
| **Capacity** | 5-10MB | 5-10MB | 50MB+ |
| **Persistence** | Until cleared | Tab close | Until cleared |
| **Async** | No | No | Yes |
| **Queryable** | No | No | Yes |
| **Transactions** | No | No | Yes |
| **Indexes** | No | No | Yes |
| **Cross-tab** | Yes | No | Yes |
| **Best Use** | Preferences | Temp state | Large data |

---

## Practical Example: Offline Todo App

```javascript
class TodoApp {
  constructor() {
    this.db = null;
    this.init();
  }

  async init() {
    this.db = await this.openDB();
  }

  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('TodoDB', 1);
      
      request.onupgradeneeded = (e) => {
        const db = e.target.result;
        if (!db.objectStoreNames.contains('todos')) {
          const store = db.createObjectStore('todos', { keyPath: 'id' });
          store.createIndex('completed', 'completed');
          store.createIndex('date', 'date');
        }
      };
      
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async addTodo(title) {
    const todo = {
      id: Date.now(),
      title,
      completed: false,
      date: new Date().toISOString()
    };

    return new Promise((resolve, reject) => {
      const tx = this.db.transaction(['todos'], 'readwrite');
      const request = tx.objectStore('todos').add(todo);
      request.onsuccess = () => resolve(todo);
      request.onerror = () => reject(request.error);
    });
  }

  async getTodos() {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction(['todos'], 'readonly');
      const request = tx.objectStore('todos').getAll();
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async toggleTodo(id) {
    return new Promise((resolve, reject) => {
      const tx = this.db.transaction(['todos'], 'readwrite');
      const store = tx.objectStore('todos');
      const getRequest = store.get(id);

      getRequest.onsuccess = () => {
        const todo = getRequest.result;
        todo.completed = !todo.completed;
        const updateRequest = store.put(todo);
        updateRequest.onsuccess = () => resolve(todo);
        updateRequest.onerror = () => reject(updateRequest.error);
      };
    });
  }
}

// Usage
const app = new TodoApp();
await app.addTodo('Learn IndexedDB');
const todos = await app.getTodos();
