indexedDBとは、ブラウザを利用したクライアントサイドデータベースで、今では非推奨となったLocal StorageやwebSQLの代替としても推奨されています。
本記事では、React(+TS)で作成したミニTodoアプリにindexedDBを組み合わせてTodoリストをデータ永続化し、MicroSoft To Doのようにオフラインアプリケーションとして機能させてみます。
- indexedDBの応用例をサンプルを通して体感できる
useReducer(), useContext()
で状態管理されたReactアプリケーションのクライアントDBへの同期の勘所
なお、本記事は以下のディレクトリ構成のアプリケーション(詳細はGithubを参照)を前提としており、本記事ではデータの管理、共有を担うstore
ディレクトリのコードについて解説していきます。
micro-todo-app/
├── components/
│ └── ...
├── hooks/
│ └── ...
└── store/
├── todos-context.tsx //アプリケーション全体に共有されるデータの管理
├── initializer.ts
└── syncData.ts // indexedDBとの同期処理
本記事では、React + TypeScript、JavaScript ES6の基本文法レベルの知識を前提としています。
クライアントデータベース(indexedDB)と同期するにあたり、アプリケーションの仕様として最低限必要なものを書いておきます。
まず、データ永続化の対象となるTodoは、型Todo
のオブジェクトで保存されており、Reactアプリケーション全体で管理されてるTodoリストのデータはTodoの配列Todo[]
で定義されています。
TodosState
はuseReducer()
フックでstateとして管理され、useContext()
フックを通してアプリケーション全体にstateとその更新処理が共有されています。
本記事で紹介しているサンプルTodoアプリでは、以下の3種類のActionを設定しています。
ADD_TODO
アクション:Todoを1つ追加するアクションです。Todo
型のオブジェクトをTodosState
型のStateに追加します。REMOVE_TODO
アクション:削除が選択されたTodoを削除するアクションです。EDIT_TODO
アクション:Todoの進行状態を表すprogress
プロパティが変更された時、変更をstateに反映するアクションです。
今回はサンプルとして、EDIT_TODO
アクションについてはprogress
プロパティの変更のみ実装していますが、他のプロパティの変更を実装した場合にも、stateへの反映やクライアントDB(indexedDB)との同期処理は全く同じです。
React、TypeScriptの話になりますので詳細は記載しませんが、同じコードパターンでアクションの追加・拡張はできますので試してみてください。
アプリケーションの仕様を押さえると、アプリケーションのデータを、React・クライアントDBの間でどのようにやり取りするべきか、以下のように考えられます。
- アプリケーションが初期化されるとき、クライアントDBにデータがあるか確認する。クライアント側にデータがあれば、クライアント側のデータでTodoリストのstateを初期化する。
ADD_TODO
アクションが発火したら、クライアントDBに同じデータを追加する。REMOVE_TODO
アクションが発火したら、クライアントDBから同じデータを削除する。EDIT_TODO
アクションが発火したら、クライアントDBから更新対象のデータを検索し、同じ更新を反映する。
言語化すると以外にもシンプルですね。
Reactアプリケーションにおいて、useReducer(), useContext()
フックで状態管理を行う場合、アプリ全体に共有されるStateと、その更新処理(Action)はuseReducer()
で初期化されます。
すると、実装の方針としては以下のようにすれば良さそうです。
- アプリケーションの初期化において、
useReducer()
に渡すinitialState
の中身を、クライアントDBから取るかどうかを分岐する処理を書き、initialState
取得時に呼び出す。 ADD_TODO
アクション発火時に実行する処理に、dispatch({ type: ..., action: ...})
だけでなく、クライアントDBにデータを追加する関数の呼び出しを追加する。ADD_TODO
について行った変更と同様に、REMOVE_TODO
アクション発火時に実行する処理に、クライアントDBのデータ削除処理を追加する。ADD_TODO
について行った変更と同様に、EDIT_TODO
アクションが発火時に実行する処理に、クライアントDBに更新を反映する処理を追加する。
実装の方針を示す疑似コードはこのようになります。
全体の方針が掴めましたので、詳細を実装していきます。今回は、以下のようにファイルを作成しました。
.
├── ├ micro-todo-app/
│ └── ├ store/
│ └── ├ todos-context.tsx // コンテクスト本体を記述するファイル
├── + ├ initializer.ts // アプリの初期Stateを返すイニシャライザーを定義
├── + ├ demoData.ts // 初期データがなかった際に初期Stateに設定するダミーデータ
└── + ├ syncData.ts // Stateの変更をクライアントDBに同期する処理を記述
なお、以下の実装例の紹介では、indexedDBの基本操作方法について(紙面の都合上)説明しません。不明点については、以下の記事を参照しながら読んでいただけると幸いです。
indexedDBの使い方 | アプリケーションデータをクライアント側に保持しようinitializer.ts
を編集して、アプリの初期Stateを取得します。
以下の分岐を考えるのでした。
アプリケーションの初期化において、useReducer()
に渡すinitialState
の中身を、クライアントDBから取るかどうかを分岐する処理を書き、initialState
取得時に呼び出す。
全体を示す疑似コードは以下のようになりますね。
全体の実装方針に基づいて、実際の実装は以下のようになりました。
syncData.ts
を編集して、アプリのStateに反応してクライアントDBのデータが同期するようにします。
ここで、ReactアプリケーションにおけるState更新の特徴を考えると、Actionの種類ごとに分岐する構造でクライアントDBとの同期処理を書けば、1つの関数で済むのでは無いかと思われます。
疑似コードは以下のように書けるかと思います。
疑似コードで立てた実装方針に基づいて、実際の実装は以下のようになりました。
実装ができたら、アプリでオフラインアプリケーションの機能を確認してみましょう。
アプリを開いたら、ブラウザの開発者モードから、アプリケーション→ストレージの順に選択して、indexedDBストレージの中にtodosオブジェクトストア(テーブルのようなもの)があるのを確認してください。
initializer.ts
の実装にあるように、ユーザーがアプリケーション上で何も操作を行なっていない段階では、demoData.ts
ファイルに用意されたダミーデータが読み込まれています。
次に、ユーザーの操作によってアプリで管理されたStateが変更されたとき、正しくクライアントDBと同期しているかを確認してみましょう。
Todoアプリにtodoを追加してみましょう。「+ Add Todo」ボタンから開くウィンドウで、todoの内容を入力し、Saveボタンを押すと新規追加が完了します。
todoアプリの見た目で追加されたことが分かりますが、追加されたtodoがクライアントDB(indexedDB)上に保存されているか確認しましょう。
先ほどと同じブラウザの開発者モード→アプリケーション→ストレージを見ると、入力内容が反映されています。
ブラウザの開発者モードを開いたままだと、更新が表示に反映されていない場合がありますので、その場合は更新ボタンを押してください。
最後に、反映されているデータが永続化されていることを確認しましょう。ページリフレッシュを行なっても、同じデータ(Todo)が反映されていれば成功です。
いかがだったでしょうか。駆け足の説明で申し訳ありません(時間があったら増補します)が、まるでサーバーにデータが保持されているかのような挙動を、オフラインでも再現することができました。
オフラインでも機能するTodo管理アプリMicrosoft To Doは、かなり利用者も多い有名アプリですが、原理的には本記事と同じようなデータ永続化を行なっています。
現実世界での、クライアント側DBでのデータ永続化によるオフラインアプリケーション対応には、以下のようなものがあるようです。
- Todoリストなど、オフラインで動作することが望ましいアプリ
- 屋外ライブ会場など、通信が不安定な環境下で一時的にクライアント側でデータ管理したい場合
- チャットルームの書きかけテキストなど、サーバーとのやり取りをするコストを払えないがデータは保持したい場合
また、クライアントDBとアプリ間でのデータのやり取りは、アプリの相手側がサーバーになっても根本的には同じ流れと言えるでしょう。
フロントエンド側だけで手軽にデータフェッチの処理を実験できますから、サーバーサイドの学習前にチュートリアルとして利用していただくのも良いかと思います。
以上で本記事の説明を終わります。お疲れ様でした。