Image

progressive-web-apps-vue-3

7 min read
Last update: December 19, 2021

Fetching a Database with IndexedDB

Now that we know what IndexedDB is, it’s time for us to learn how to interact with it. We’ll start by learning how to fetch the database directly.

Creating Your First Database

To get started, let’s open up the DevTools and call the following function in the console.

window.indexedDB.open('alphaDb', 1)

Remember how we learned that IndexedDB is a native part of the browser? This means that we can access it directly via the window object in the console! Once we’ve done that, we’ll be calling the open method on IndexedDB which starts a request.

What is a request?

A request is what we are asking from IndexedDB. What are we requesting exactly? Well, we’re requesting to open a database within IndexedDB! If one does not exist yet, it will create one. Otherwise, it will fetch us the instance that we requested.

Let’s create another database by running the following command:

window.indexedDB.open('betaDb', 1)

One thing you might have noticed when we call the open function is that it takes two arguments: (1) the name of the database, and (2) the version of the database.

For the purpose of this lesson, you can assume that all our databases will be version 1. In the event you want to upgrade existing databases, this is when versions will come into play.

Now that you know how to create databases, go ahead and create gammaDb for practice. Once you’ve done that, it’s time for us to learn the next step of our journey, using lifecycle hooks to add additional requests to the database.

What are request lifecycle hooks?

Similar to how Vue component have lifecycle hooks (e.g., mounted, created, etc.), database requests also have their own lifecycle hooks. For this lesson, we will be focusing on two primary ones: onerror and onsuccess.

onerror Lifecycle Hook

This is the “On Error” hook. When something goes wrong, it will call the function we assign to it. Here is a simple example of how we can log an error to the console when the request runs into an error:

let request = window.indexedDB.open('todomvcdb', 1)

request.onerror = event => {
console.error('ERROR: Unable to open database', event)
}

Similar to other events in JavaScript, the lifecycle hook is also passed the event as the default argument so that we can call it in our function.

This lifecycle hook is important because we want to know when something goes wrong.

onsuccess Lifecycle Hook

request.onsuccess = event => {
console.log('SUCCESS: Database opened successfully', event)
}

Creating a Reusable Method to Fetch Our Database

While what we’ve learned is great for direct interaction with the database, we need a reusable and programmatic way for us to fetch our database. And the way that we can accomplish this is through the use of JavaScript Promises. I’ll be covering the syntax and how we do it at a high level in this lesson to ensure we’re all on the same page, but for more details, be sure to check out the official docs.

Scaffolding Our Promise

To start, let’s scaffold a function called getDatabase. This will be an async function because there is no way to guarantee when IndexedDB will finish its work. So we want to allow it to run in a separate process without blocking JavaScript.

const getDatabase = async () => {

}

Next, our function will return a Promise because we will need the ability to chain off of the response that IndexedDB gives us. In other words, similar to an API request, we’d like to be able to say something like: getDatabase().then() and so forth. To do this, we return a new Promise which accepts a callback function.

export default {
methods: {
async getDatabase() => {
return new Promise((resolve, reject) => {

})
}
}
}

Now here’s the slightly tricky part if you’re new to Promises, the callback function comes with two default parameters: resolve and reject. At a high level, it allows you to programmatically set condition where a promise can be “resolved” (i.e., successful) or “rejected” (i.e., failed).

export default {
methods: {
async getDatabase() => {
return new Promise((resolve, reject) => {
if (true) {
return resolve('Success')
} else {
return reject('Error')
}
})
}
}
}

In the example above, we’ve simplified the conditional so that this promise will always resolve, but you can see how we could call the resolve and reject to finish our promise scaffold. With this basic scaffold, let’s integrate what we’ve learned about fetching databases from IndexedDB.

Check for existence of a database

We’ll start by declaring a database in data to keep track of whether we were previously successful in fetching the database. Another way of seeing the database variable is that it is caching the response.

export default {
data: () => ({
database: null
}),
methods: {
async getDatabase() => {
return new Promise((resolve, reject) => {
if (true) {
return resolve('Success')
} else {
return reject('Error')
}
})
}
}
}

With this new variable, we can now update our conditional for resolve by checking for the existence of the database. If it does exist, we can resolve the promise as expected!

export default {
data: () => ({
database: null
}),
methods: {
async getDatabase() => {
return new Promise((resolve, reject) => {
if (this.database) {
return resolve(this.database)
} else {
return reject('Error')
}
})
}
}
}

However, we’re not done yet! It’s time for us to add the code to request the database.

Request IndexedDB for a database

We’ll start be creating a request to open / create our desired database. For our app, I have named our database todomvcdb.

let database

const getDatabase = async () => {
return new Promise((resolve, reject) => {
if (database) {
return resolve(database)
} else {
return reject('Error')
}

let request = window.indexedDB.open('todomvcdb', 1)
})
}

You might have noticed that this code doesn’t make that much sense yet. After all, if no database exists, we immediately reject the promise before we even make the request. It’s time for us to update it using the lifecycle hooks we learned about in this lesson.

Leveraging the onerror lifecycle hook

When it comes to rejecting our promise, nothing is more appropriate for this conditional than when the onerror lifecycle hook is called. So let’s go ahead and update it accordingly while also adding a console log to alert of us of the error:

export default {
data: () => ({
database: null
}),
methods: {
async getDatabase() => {
return new Promise((resolve, reject) => {
if (this.database) {
return resolve(this.database)
}

let request = window.indexedDB.open('todomvcdb', 1)

request.onerror = event => {
console.error('ERROR: Unable to open database', event)
reject('Error')
}
})
}
}
}

With that, we have one more step to complete our getDatabase function: what do we do if we are successful in fetching our database? Time for our onsuccess hook!

Leveraging the onsuccess lifecycle hook

Now that we have our error handling, it’s time to actually assign our database once we receive is successfully! As you might have guessed, that means it’s time for the onsuccess hook!

Inside of our hook, we’ll want to make sure that we assign the database to the database

export default {
data: () => ({
database: null
}),
methods: {
async getDatabase() => {
return new Promise((resolve, reject) => {
if (this.database) {
return resolve(this.database)
}

let request = window.indexedDB.open('todomvcdb', 1)

request.onerror = event => {
console.error('ERROR: Unable to open database', event)
reject('Error')
}

request.onsuccess = event => {
this.database = event.target.result
resolve(this.database)
}
})
}
}
}

Next Steps

Congratulations! You’ve taken a big step in learning how to work with IndexedDB. If you’re new to JavaScript Promises, I know it can feel like a lot, but there’s a lot of value in learning these concepts as you grow your JavaScript abilities. In the next lesson, you’ll be learning about Object Stores and how they serve as the core of how raw data is stored, retrieved, and modified. See you then!