IndexedDBを利用したオンライン/オフライン時におけるデータ管理
業務で indexedDB というブラウザ内でデータ保存することができるデータベースを使ったので、そのアウトプットとして記事にまとめました。少しでも参考になれば幸いです。
🤖 IndexedDBとは?
IndexedDBは、ブラウザ内で使用できる高度なデータベースシステムです。
クライアント側で動作し、大量のデータを効率的に扱うことが可能なNoSQLデータベースです。
IndexedDBは構造化されたデータをキーとともに保存し、オフライン状態でもデータの読み書きが可能です。
また、非同期APIを利用しているため、データの取得や更新を行う際に、ブラウザの主要な動作を妨げることなく操作を行えます。
そのため、UX向上、パフォーマンス向上にもつながります。
さらに、IndexedDBは「トランザクション」という機能を備えており、これにより複数のデータ操作を同時に行っても、データが正確かつ安全に扱うことが可能です。
まとめると
IndexedDB | |
---|---|
データの生存期間 | 手動で消去するまで |
データ保存可能容量 | ブラウザによる制限なし(ディスク容量に基づく) |
データアクセス可能領域 | クライアントのみ |
データ形式 | 任意のデータ形式(構造化されたデータも含む) |
通信 | 送信しない |
主な利用用途 | 大量かつ複雑なデータの長期保存、オフライン対応アプリケーション |
以下の記事を引用させていただきました。
👨🏫 ユースケース
主なユースケースとしては以下の6点が挙げられます。
- オフラインデータストレージ: Webアプリケーションがオフラインでも機能するように、ユーザーデータやアプリケーションデータをブラウザに保存します。これにより、ネットワーク接続がない場合でも、アプリケーションは引き続きユーザーにサービスを提供できます。
- 大量のデータ処理: IndexedDBは大量のデータを効率的に扱うことができるため、大規模なデータセットを扱うアプリケーションに適しています。例えば、分析ツールやデータ集計ツールなどで利用されます。
- ユーザー設定とプリファレンスの保存: ユーザーのカスタム設定やプリファレンスを保存し、次回の訪問時にこれらの設定を復元するために使用されます。
- キャッシングと一時データストレージ: Webアプリケーションがサーバーから受け取ったデータをキャッシュするために利用され、より迅速なデータアクセスとパフォーマンスの向上を実現します。
- プログレッシブウェブアプリ(PWA): PWAはオフライン機能やデータの永続性に重点を置いており、IndexedDBはこれらのアプリケーションでデータを管理するための主要なツールです。
- ゲームの状態管理: オンラインゲームやブラウザゲームにおいて、プレイヤーの進行状況や設定を保存するために使用されます。
(ChatGPTくんに聞いた回答)
今回実装する内容は、実際に業務で扱ったオフライン時におけるデータ管理についてまとめていきたいと思います。
🛠️ 初期設定
/**
* @description indexedDBの初期化処理
* @module utils/indexedDB
* @return {Promise<IDBDatabase>}
* @reference https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API/Using_IndexedDB
*/
export const INDEXED_DB_NAME = "test-idb";
export const USER_DATA_STORE = "userDataStore";
let idb: IDBDatabase;
export const openIndexedDB = async (): Promise<IDBDatabase> => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(INDEXED_DB_NAME, 2);
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
/**
* @memo :
* オブジェクトストアの作成および削除や、インデックスの構築および削除が可能領域
*/
idb = (event.target as IDBOpenDBRequest).result;
if (!idb.objectStoreNames.contains(USER_DATA_STORE)) {
// * オブジェクトストアの作成
idb.createObjectStore(USER_DATA_STORE, { keyPath: "id" });
}
};
request.onsuccess = (event: Event) => {
console.log("Database opened successfully");
idb = (event.target as IDBOpenDBRequest).result;
resolve(idb);
};
request.onerror = error => {
console.error("Database error:", error);
reject(error);
};
});
};
今回の最終ゴールは、添付画像のようにデータベースとオブジェクトストアが格納されていれば成功です!🙆♂️
📝 機能要件
1.indexedDBにusersデータを追加
2.indexedDBにuserデータが存在する場合はindexedDBからデータを取得して、存在しない場合はswrを使ってRoute Hnadlerでデータを取得する
3.再検証処理を行う
🚀 実装手順
以下は簡易的なサンプルコードです。
実際にDBと紐付けたり、API処理を実装することで応用的に使うことができます。
データの取得処理
import useSWRImmutable from "swr/immutable";
const fetcher = async (userId: string): Promise<UserData | any> => {
try {
const idb = await openIndexedDB();
const transaction = idb.transaction([USER_DATA_STORE], "readonly");
const store = transaction.objectStore(USER_DATA_STORE);
const request = store.get(userId);
return new Promise((resolve, reject) => {
request.onsuccess = async () => {
if (request.result) {
resolve(request.result);
} else {
try {
const res = await fetch(`/api/user/${userId}`);
if (!res.ok) {
reject(new Error(res.statusText));
} else {
const data = await res.json();
resolve(data);
}
} catch (error) {
reject(error);
}
}
};
request.onerror = () => reject(request.error);
});
} catch (error) {
throw error;
}
};
// 省略...
const { data, mutate, error } = useSWRImmutable(`/api/user/${userId}`, () => fetcher(userId));
swrのfetcher関数部分を上記のように実装することで、初回ロードでAPIデータを取得した後、2回目以降はindexedDBからデータを取得するができ、オフライン時でもuserデータの取得が実現できます。
データの追加処理
useEffect(() => {
// * 初回レンダリング時に実行
const initialRendering = async () => {
const idb = await openIndexedDB();
if (idb) {
const transaction = idb.transaction([USER_DATA_STORE], "readwrite");
const store = transaction.objectStore(USER_DATA_STORE);
const request = store.get(userId);
request.onsuccess = async () => {
if (!request.result) {
const userData: UserData = {
userId,
email: "test@example.com",
firstName: "John",
lastName: "Doe",
age: 30,
};
store.add({ id: userId, userData });
}
};
request.onerror = event => {
console.error("Error fetching user data:", event);
};
}
};
initialRendering();
}, []);
初回時のみuseEffectを使ってAPIで取得したデータをindexedDBに格納します。
こうすることで永続的にuserデータをAPIを呼び出さずに取得することができます。
データの更新処理
const update = async () => {
/**
* @description IndexedDB に保存されているユーザーデータを更新する
*/
try {
const idb = await openIndexedDB();
if (idb) {
const transaction = idb.transaction([USER_DATA_STORE], "readwrite");
const store = transaction.objectStore(USER_DATA_STORE);
const request = store.get(userId);
request.onsuccess = async () => {
const data = request.result;
if (data) {
data.userData.age = 24;
store.put(data);
// * 再検証処理
await mutate();
}
};
request.onerror = event => {
console.error("Error updating user data:", event);
};
}
} catch (error) {
console.log(error);
}
};
put
を使用してindexedDB内の更新処理が可能です。また、mutate()
を併せて書くことで再検証処理もしています。
全体コード
type UserData = {
userId: string;
email: string;
firstName: string;
lastName: string;
age: number;
};
const fetcher = async (userId: string): Promise<UserData | any> => {
try {
const idb = await openIndexedDB();
const transaction = idb.transaction([USER_DATA_STORE], "readonly");
const store = transaction.objectStore(USER_DATA_STORE);
const request = store.get(userId);
return new Promise((resolve, reject) => {
request.onsuccess = async () => {
if (request.result) {
resolve(request.result);
} else {
try {
const res = await fetch(`/api/user/${userId}`);
if (!res.ok) {
reject(new Error(res.statusText));
} else {
const data = await res.json();
resolve(data);
}
} catch (error) {
reject(error);
}
}
};
request.onerror = () => reject(request.error);
});
} catch (error) {
throw error;
}
};
export default function TestPage({}) {
const userId = "b1fb6cb1-d59b-032c-da88-7ae2dda0eeb8";
const { data, mutate, error } = useSWRImmutable(`/api/user/${userId}`, () => fetcher(userId));
const userData = data?.userData;
useEffect(() => {
// * 初回レンダリング時に実行
const initialRendering = async () => {
const idb = await openIndexedDB();
if (idb) {
const transaction = idb.transaction([USER_DATA_STORE], "readwrite");
const store = transaction.objectStore(USER_DATA_STORE);
const request = store.get(userId);
request.onsuccess = async () => {
if (!request.result) {
const userData: UserData = {
userId,
email: "test@example.com",
firstName: "John",
lastName: "Doe",
age: 30,
};
store.add({ id: userId, userData });
}
};
request.onerror = event => {
console.error("Error fetching user data:", event);
};
}
};
initialRendering();
}, []);
const update = async () => {
/**
* @description IndexedDB に保存されているユーザーデータを更新する
*/
try {
const idb = await openIndexedDB();
if (idb) {
const transaction = idb.transaction([USER_DATA_STORE], "readwrite");
const store = transaction.objectStore(USER_DATA_STORE);
const request = store.get(userId);
request.onsuccess = async () => {
const data = request.result;
if (data) {
data.userData.age = 24;
store.put(data);
// * 再検証処理
await mutate();
}
};
request.onerror = event => {
console.error("Error updating user data:", event);
};
}
} catch (error) {
console.log(error);
}
};
// * エラー時に実行
if (error) return <div>Error</div>;
return (
<div>
<h1>Test Page</h1>
<Button onClick={update}>更新</Button>
</div>
);
}
👀 おまけ
弊社では、スマホやPC1つで完結する網羅的な教材と、無制限で解ける本番と同形式の模試で、短期間での資格取得を目指すことができる簿記のアプリ 『Funda簿記』 を運営しています。
少しでも興味のある方がいれば、リンクよりアクセスしていただくか、メールにてお願いします☺️
Discussion