How to Add Client-Side Storage with Vue Part 2

Take client-side storage to the next level with IndexedDB!

Maximilien Monteil
6 min readFeb 10, 2019

Taking things further

In my previous post we took our Todo list application and upgraded it with support for client-side storage using the localStorage API.

We then went a step further and added Vuex to the app. This let us separate concerns even more and make our storage solution very modular.

In this article, we will once again move forward with our Todo app and implement client-side storage with IndexedDB.

IndexedDB is a lower level client than localStorage but it can store significant amounts of data. Now none of these might sound beneficial for our app, after all it is a very simple todo list, but in this series, we’re in the business of over engineering, additionally one future idea could be to turn this into a PWA which will require the use of IndexedDB.

By the end of the article, we’ll modify the app to default to using IndexedDB for storage and fallback to localStorage if the browser doesn’t support it or there was some error.

IndexedDB overview

IndexedDB is the API to use for client-side storage if you want to store complex data, files, or even blobs in the browser. If we one day want to turn our app into a PWA, we need to have storage implemented using IndexedDB.

The way this database works is completely different from localStorage and comes with some extra complexity. It is a transaction based database system that is described by MDN as a JavaScript-based object-oriented database.

What this means is that data isn’t stored in a table with rows and columns as with SQL-based RDBMS but rather with JavaScript objects that are accessible/indexed with a key.

Find out more at MDN

Old School DB

I described IndexedDB as being transaction based and this has huge ramifications on how we work with it.

Modifying elements in the database first means creating a transaction and making a request to do the operation. We then need to listen for the right kind of DOM event.

In today’s JavaScript landscape this method of interaction might seem rather clunky, we expect to instead be dealing with promises. Luckily there’s a library by Jake Archibald that wraps up IndexedDB with promises.

IndexedDB Concepts

IndexedDB works in a rather different way from other database systems, before continuing here are a few terms and what they mean in this context.

  • Database — First level of IndexedDB, this is where all the object stores are kept. You can create as many databases as you want but usually a single one per app is sufficient.
  • Transaction — Every action in IndexedDB works through a transaction. In order to access any action you’ll first need to create a transaction off of which you will then listen for events on completion. They are useful to maintain the integrity, if one of the operation fails, the whole action is cancelled. The idb library wraps these up in promises (among other things).
  • Object Store — Object stores are similar to tables or relations in traditional relational databases. Each is meant to hold one kind of entity. For our app we’ll make two stores, one for “Todos” and one for “Completed” todos.

There are a few other concepts to know when working with IndexedDB but these are the ones we’ll use for the project.

IndexedDB concepts at MDN

Setup

If you want to follow along clone the vuexRefactor branch from the git repo with the following command:

$ git clone -b vuexRefactor --single-branch https://github.com/MaxMonteil/VueTodo.git

The final project is available from the indexedDB branch.

$ git clone -b indexedDB --single-branch https://github.com/MaxMonteil/VueTodo.git

Next install idb to make it easier to work with IndexedDB.

$ npm install idb

Once your app first creates a database on your browser it won’t change unless you update the version or delete it. Instead of dealing with versions while you’re still setting it up, you might prefer to delete the whole database and then reload the page.

To do that in Chrome, go into the developer console in the ‘Application’ tab, click on IndexedDB and then you should see the database name you chose. There you can delete or refresh the database.

Initializing IndexedDB

Create a new indexedDBService.js file in the api/ folder.

The first thing to do is to create our database and it’s object stores.

You’ll see that we first check if the browser supports IndexedDB, if it doesn’t we return an error that we will deal with later in the Vuex store.

The openDbmethod takes the name of the database, the version, and a callback that will initialize the database and define the schemas.

What the version does isn’t particularly useful for our small app but it helps ensure our application database is up to date, it is also the only point in time when it is possible to update or change it’s schema.

* Since writing this article idb has had a new update that changes the function names from *Db to *DB so it’s openDB now. Thank you Willy Kurmann for noticing!

// if installed from npm use 'openDB'
import { openDb } from ‘idb’
const dbPromise = _ => {
if (!(‘indexedDB’ in window)) {
throw new Error(‘Browser does not support IndexedDB’)
}
// if installed from npm use 'openDB'
return openDb(‘VueTodoDB’, 1, upgradeDb => {
if (!upgradeDb.objectStoreNames.contains(‘todos’)) {
upgradeDb.createObjectStore(‘todos’)
}
if (!upgradeDb.objectStoreNames.contains(‘completed’)) {
upgradeDb.createObjectStore(‘completed’)
}
})
}

Managing Data

In the previous version of the app where we used localStorage, we had two methods, one to save data to storage and another to check storage aptly named checkStorage and saveToStorage respectively.

To benefit from this modularity we want to make sure we export the same methods, let’s implement them.

Saving to Storage

To save to storage we’ll just do as we did with localStorage by overwriting storage since our data is very simple. We use the put method instead of add, this lets us create new items but also overwrite them.

We also get to see transactions in action.

First off, we can only call the put method off of a transaction object as they’re in charge of managing IndexedDB operations.

Lastly it’s the transaction that we actually return in a promise which will resolve when the operation completes, either successfully or not.

const saveToStorage = async (storeName, tasks) => {
try {
const db = await dbPromise()
const tx = db.transaction(storeName, ‘readwrite’)
const store = tx.objectStore(storeName)
store.put(tasks, storeName) return tx.complete
} catch (error) {
return error
}
}

Checking Storage

const checkStorage = async storeName => {
try {
const db = await dbPromise()
const tx = db.transaction(storeName, ‘readonly’)
const store = tx.objectStore(storeName)
return store.get(storeName)
} catch (error) {
return error
}
}

This function is simply going to get the data from the object store we created earlier, since we just overwrite it each time there’s no need for an index.

Finally don’t forget to export both functions.

export default {
checkStorage,
saveToStorage
}

Making it all work

We’ve now set up IndexedDB in it’s own service file, we just need to link it up with our Vuex store.

The goal here is to add IndexedDB as our main storage solution and use localStorage as a fallback. We’ll rewrite our store checkStorage and saveTodos methods.

import ls from ‘./api/localStorageService’
import idbs from ‘./api/indexedDBService’
export default new Vuex.Store({
actions: {
checkStorage ({ state, commit }) {
state.dataFields.forEach(async field => {
try {
let data = await idbs.checkStorage(field)
// IndexedDB did not find the data, try localStorage
if (data === undefined) data = ls.checkStorage(field)
// LocalStorage did not find the data, reset it
if (data === null) data = []
commit(‘setState’, { field, data })
} catch (e) {
// The value in storage was invalid or corrupt so just set it to blank
commit(‘setState’, { field, data: [] })
}
})
},
async saveTodos ({ state }) {
try {
await Promise.all(state.dataFields.map(field => {
return idbs.saveToStorage(field, state[field]))
})
} catch (e) {
state.dataFields.forEach(field => {
return ls.saveToStorage(field, state[field])
})
}
}
}
})

Now we get to see the benefits of using Vuex and separating concerns. No changes needed to be made to our Vue components and the changes we made to the store were only to enable the use of both storage options.

No matter how simple your application is, a proper architecture can save you hours of trouble, effort, and pain.

Bringing it all Together

We’re done!

Now our app can use 2 different client-side storage solutions depending on what the browser supports.

If you’re on your way to building a PWA or need a client-side storage option for more complex data I hope this guide has helped you get started. If you want to learn more check out these resources:

--

--

Maximilien Monteil

My goal is to solve problems people face with beautiful, practical, and useful solutions. I’m driven to apply and share what I learn.