IndexedDB—Your Browser’s Secret Storage Superpower

HarshalHarshal
7 min read

Introduction

Imagine you're building a slick web app, and you need a place to store data directly in the user's browser—no server delays, no waiting for a connection. Enter IndexedDB, the browser’s hidden storage hero. It’s like a mini fridge for your app: stuff in tons of structured data (think JSON, images, or anything you need), and retrieve it at lightning speed—even when the Wi-Fi decides to take a break.

But here’s the catch: it’s powerful, but kind of quirky, like that friend who's great once you figure them out. By the end of this blog, you’ll be handling IndexedDB like a pro. Ready? Let’s dive in!


What is IndexedDB, Anyway?

IndexedDB is your browser’s very own database—a hidden gem for developers who need more than the measly key-value pairs of localStorage or sessionStorage. Picture a filing cabinet tucked into Chrome, Firefox, or whatever browser you’re vibing with. It’s asynchronous (meaning it works behind the scenes so your app stays snappy), and it can handle serious data—think JSON objects, images, or even entire files—without blinking.

Why should you care? IndexedDB powers real-world magic like offline functionality in Progressive Web Apps (PWAs), caching chunky datasets to turbocharge performance, or storing user inputs for a seamless experience when the Wi-Fi flakes out. It’s the difference between a Post-it note (localStorage) and a full-blown spreadsheet (IndexedDB). Need your app to shine offline or search piles of data fast? IndexedDB’s your MVP.


How Does This Thing Work?

Let’s break it down without drowning in tech jargon. Here’s what makes IndexedDB tick:

  • Database: The big boss—the box where all your stuff lives. You name it, create it, and stuff it full.

  • Object Stores: Little drawers inside the box. Each one holds related items—like a “friends” drawer with names and ages.

  • Indexes: Sticky tabs for quick lookups. Flip straight to “everyone over 25” without rummaging through everything.

  • Transactions: The safety net. It’s like, “Do these three things, and if anything flops, undo it all.” No data disasters here.

It’s less “SQL” and more “browser magic with a side of organization.” Ready to see it in action? Let’s set it up!


Setting Up Your IndexedDB Playground

Before we start tossing data around, we need to build the database. Since we’re rocking TypeScript for that sweet type safety, here’s how to kick things off:

const request: IDBOpenDBRequest = indexedDB.open('MyDatabase', 1); // Open sesame—or make it if it’s not there

request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
  const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
  // Make a "friends" drawer with auto-numbered IDs
  const objectStore: IDBObjectStore = db.createObjectStore('friends', { keyPath: 'id', autoIncrement: true });

  // Add sticky tabs for quick lookups
  objectStore.createIndex('name', 'name', { unique: false });
  objectStore.createIndex('age', 'age', { unique: false });
};

request.onsuccess = (event: Event) => {
  const db: IDBDatabase = (event.target as IDBOpenDBRequest).result;
  console.log('Database ready!', db);
};

indexedDB.open() knocks on the door. No database? It whips one up.

onupgradeneeded is your setup party—runs when the database is born or gets a glow-up. You’re just prepping your drawers and tabs here, with TypeScript keeping your types in line.

Now that we’ve set up our database, let’s dive into how we can interact with it in our app.


Playing with IndexedDB: The Basics

With our database ready, it’s time to have some fun—adding, grabbing, tweaking, and yeeting data. First, let’s define a Friend type for clarity:

interface Friend {
  id?: number; // Optional because autoIncrement handles it
  name: string;
  age: number;
}
  1. Adding Stuff (Because Hoarding is Fun)

Let’s toss a friend in:

const addFriend = (db: IDBDatabase, friend: Friend) => {
  const transaction: IDBTransaction = db.transaction('friends', 'readwrite');
  const objectStore: IDBObjectStore = transaction.objectStore('friends');
  const request: IDBRequest<IDBValidKey> = objectStore.add(friend);

  request.onsuccess = () => console.log('Friend added:', friend);
};

Boom! Your friend’s in, and autoIncrement handles the ID. TypeScript ensures everything’s legit.

  1. Grabbing Stuff (Where’s My Friend At?)

Need to peek at someone? Here’s the move:

const getFriend = (db: IDBDatabase, id: number) => {
  const transaction: IDBTransaction = db.transaction('friends', 'readonly');
  const objectStore: IDBObjectStore = transaction.objectStore('friends');
  const request: IDBRequest<Friend> = objectStore.get(id);

  request.onsuccess = (event: Event) => {
    const friend: Friend | undefined = (event.target as IDBRequest<Friend>).result;
    console.log('Found friend:', friend);
  };
};

It’s like asking, “Gimme file #1,” and getting it slid right over—typed as a Friend.

  1. Tweaking Stuff (Aging Gracefully)

Birthday time? Update them:

const updateFriend = (db: IDBDatabase, friend: Friend) => {
  const transaction: IDBTransaction = db.transaction('friends', 'readwrite');
  const objectStore: IDBObjectStore = transaction.objectStore('friends');
  const request: IDBRequest<IDBValidKey> = objectStore.put(friend);

  request.onsuccess = () => console.log('Friend updated:', friend);
};

put() swaps in the new version—updates if the ID exists, adds if it doesn’t.

  1. Yeeting Stuff (Sorry, Pal)

Time to part ways:

const deleteFriend = (db: IDBDatabase, id: number) => {
  const transaction: IDBTransaction = db.transaction('friends', 'readwrite');
  const objectStore: IDBObjectStore = transaction.objectStore('friends');
  const request: IDBRequest<undefined> = objectStore.delete(id);

  request.onsuccess = () => console.log('Friend deleted:', id);
};

Poof! They’re gone—clean and simple.

These basics are your bread and butter. Next, let’s see how transactions keep everything in sync.


Transactions: The Group Project Rule

Everything in IndexedDB happens in transactions—like a group project where if one step flops, the whole thing’s scrapped. Here’s a combo move:

const addMultipleFriends = (db: IDBDatabase, friends: Friend[]) => {
  const transaction: IDBTransaction = db.transaction('friends', 'readwrite');
  const objectStore: IDBObjectStore = transaction.objectStore('friends');

  friends.forEach((friend) => objectStore.add(friend));

  transaction.oncomplete = () => console.log('High five! Everyone’s in!');
};

If anyone doesn’t make it, no one does. Keeps your data neat. Now, let’s level up with some pro searching.


Searching Like a Pro

Indexes are your secret weapon for fast finds. Wanna see who’s over 20?

const findFriendsOverAge = (db: IDBDatabase, minAge: number) => {
  const transaction: IDBTransaction = db.transaction('friends', 'readonly');
  const objectStore: IDBObjectStore = transaction.objectStore('friends');
  const index: IDBIndex = objectStore.index('age');

  const request: IDBRequest<IDBCursorWithValue | null> = index.openCursor(IDBKeyRange.lowerBound(minAge));

  request.onsuccess = (event: Event) => {
    const cursor: IDBCursorWithValue | null = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
    if (cursor) {
      console.log('Found:', cursor.value as Friend);
      cursor.continue(); // Next, please!
    }
  };
};

It’s like flipping through your drawer with a “20+ only” filter—smooth and typed. Ready to bring this into the real world with React? Let’s go!


IndexedDB with React and TypeScript

Since it’s 2025 and React with TypeScript is the dream team, here’s how to use IndexedDB in a component:

import React, { useEffect, useState } from 'react';

interface Friend {
  id?: number;
  name: string;
  age: number;
}

const App: React.FC = () => {
  const [db, setDb] = useState<IDBDatabase | null>(null);
  const [friends, setFriends] = useState<Friend[]>([]);

  useEffect(() => {
    const request: IDBOpenDBRequest = indexedDB.open('MyDatabase', 1);

    request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
      const database: IDBDatabase = (event.target as IDBOpenDBRequest).result;
      const objectStore = database.createObjectStore('friends', { keyPath: 'id', autoIncrement: true });
      objectStore.createIndex('name', 'name', { unique: false });
      objectStore.createIndex('age', 'age', { unique: false });
    };

    request.onsuccess = (event: Event) => {
      setDb((event.target as IDBOpenDBRequest).result);
    };

    request.onerror = () => console.error('Database error:', request.error);
  }, []);

  const addFriend = (friend: Friend) => {
    if (!db) return;
    const transaction = db.transaction('friends', 'readwrite');
    const objectStore = transaction.objectStore('friends');
    const request = objectStore.add(friend);
    request.onerror = () => console.error('Add failed:', request.error);
    transaction.oncomplete = () => fetchFriends();
  };

  const fetchFriends = () => {
    if (!db) return;
    const transaction = db.transaction('friends', 'readonly');
    const objectStore = transaction.objectStore('friends');
    const request = objectStore.getAll();

    request.onsuccess = (event: Event) => {
      setFriends((event.target as IDBRequest<Friend[]>).result);
    };
  };

  useEffect(() => {
    if (db) fetchFriends();
  }, [db]);

  return (
    <div>
      <h1>Friends List</h1>
      <button onClick={() => addFriend({ name: 'John Doe', age: 30 })}>
        Add John
      </button>
      <ul>
        {friends.map((friend) => (
          <li key={friend.id}>{`${friend.name}, ${friend.age}`}</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

This sets up your database, adds friends, and lists them—all with TypeScript’s safety net and React’s component flair. Click the button, and John’s in the club! Notice the onerror handler—it’s a simple way to catch hiccups. Speaking of which…


The “Ugh” Parts of IndexedDB

It’s not all sunshine:

  • Asynchronous Chaos: Stuff happens in the background, so you’re juggling callbacks like a circus clown (even in TypeScript).

  • Error Drama: Things can fail, and you need to handle it. Add an onerror like request.onerror = () => console.error('Oops:', request.error) to stay sane.

  • Wordy Weirdness: The API’s clunky—like texting your grandma, it takes extra steps.


When Should You Even Bother?

Use IndexedDB when:

  • You’ve got tons of data (localStorage would choke).

  • You’re wrangling fancy structured stuff (beyond “key: value”).

  • Your app needs to rock offline—like a PWA champ.

  • You want speedy searches through big data piles.


You can find the full code for this IndexedDB tutorial on my GitHub here.

Wrap-Up

IndexedDB is your browser’s secret sauce for managing data like a pro. It’s perfect for offline apps, performance boosts, or just avoiding server spam. Sure, it’s a diva with its callbacks and transactions, but TypeScript tames it, and React makes it a party. Next time, we’ll explore Dexie.js to make IndexedDB even smoother—less “argh” and more “aha!” So, why not give it a spin? Try implementing IndexedDB in your next app and see how it transforms your data game. Let me know how it goes!

0
Subscribe to my newsletter

Read articles from Harshal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harshal
Harshal