Tips & Tricks

Claude CodeでIndexedDBを活用したローカルストレージを実装する方法

Claude Codeを使ってIndexedDBによる構造化データの保存・検索・同期を効率的に実装する方法を解説します。

IndexedDBを選ぶ理由

localStorageは手軽ですが、容量制限(5MB程度)やデータ構造の制約があります。IndexedDBは数百MB以上のデータを構造化して保存でき、インデックスによる高速検索も可能です。Claude Codeを使えば、IndexedDBの複雑なAPIをラップした使いやすいデータアクセス層を構築できます。

型安全なIndexedDBラッパー

> IndexedDBを型安全に操作できるラッパークラスを作って。
> CRUD操作とインデックス検索に対応して。
interface StoreConfig {
  name: string;
  keyPath: string;
  indexes?: { name: string; keyPath: string; unique?: boolean }[];
}

class Database {
  private db: IDBDatabase | null = null;

  constructor(
    private name: string,
    private version: number,
    private stores: StoreConfig[]
  ) {}

  async open(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.name, this.version);

      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        for (const store of this.stores) {
          if (!db.objectStoreNames.contains(store.name)) {
            const objectStore = db.createObjectStore(store.name, { keyPath: store.keyPath });
            store.indexes?.forEach((idx) => {
              objectStore.createIndex(idx.name, idx.keyPath, { unique: idx.unique });
            });
          }
        }
      };

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

  private getStore(name: string, mode: IDBTransactionMode) {
    if (!this.db) throw new Error('データベースが開かれていません');
    return this.db.transaction(name, mode).objectStore(name);
  }

  async put<T>(storeName: string, data: T): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = this.getStore(storeName, 'readwrite').put(data);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  async get<T>(storeName: string, key: IDBValidKey): Promise<T | undefined> {
    return new Promise((resolve, reject) => {
      const request = this.getStore(storeName, 'readonly').get(key);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async getAll<T>(storeName: string): Promise<T[]> {
    return new Promise((resolve, reject) => {
      const request = this.getStore(storeName, 'readonly').getAll();
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  async delete(storeName: string, key: IDBValidKey): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = this.getStore(storeName, 'readwrite').delete(key);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  async findByIndex<T>(storeName: string, indexName: string, value: IDBValidKey): Promise<T[]> {
    return new Promise((resolve, reject) => {
      const index = this.getStore(storeName, 'readonly').index(indexName);
      const request = index.getAll(value);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }
}

実際のアプリケーションでの使用例

// データベース定義
const db = new Database('myApp', 1, [
  {
    name: 'notes',
    keyPath: 'id',
    indexes: [
      { name: 'by-category', keyPath: 'category' },
      { name: 'by-updated', keyPath: 'updatedAt' },
    ],
  },
]);

// 初期化
await db.open();

// ノートの保存
await db.put('notes', {
  id: crypto.randomUUID(),
  title: '会議メモ',
  content: '本日の議題...',
  category: 'work',
  updatedAt: Date.now(),
});

// カテゴリで検索
const workNotes = await db.findByIndex('notes', 'by-category', 'work');

サーバーとの同期

class SyncManager {
  private db: Database;

  constructor(db: Database) {
    this.db = db;
  }

  async syncToServer(storeName: string, apiEndpoint: string) {
    const pending = await this.db.findByIndex<any>(storeName, 'by-sync', 'pending');

    for (const item of pending) {
      try {
        await fetch(apiEndpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(item),
        });
        await this.db.put(storeName, { ...item, syncStatus: 'synced' });
      } catch {
        console.log('同期失敗。オンライン復帰時に再試行します。');
        break;
      }
    }
  }
}

// オンライン復帰時に自動同期
window.addEventListener('online', () => {
  syncManager.syncToServer('notes', '/api/notes/sync');
});

Reactフックとの統合

function useIndexedDB<T>(storeName: string, key?: IDBValidKey) {
  const [data, setData] = useState<T | T[] | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    const load = key
      ? db.get<T>(storeName, key)
      : db.getAll<T>(storeName);

    load.then(setData).finally(() => setLoading(false));
  }, [storeName, key]);

  return { data, loading };
}

まとめ

Claude Codeを使えば、IndexedDBの複雑なAPIを型安全にラップし、オフライン対応やサーバー同期まで効率的に実装できます。Service Workerとの連携はService Worker活用を、キャッシュ戦略はキャッシュ戦略を参照してください。

IndexedDB APIの仕様はMDN Web Docs - IndexedDBをご覧ください。

#Claude Code #IndexedDB #ローカルストレージ #オフライン #TypeScript