Original text: blog. Zhangbing. Site / 2021/03/16 /…

Offline storage of application data has become a requirement in modern Web development. The built-in browser localStorage can be used as a data store for simple, lightweight data, but is inadequate for structured data or for storing large amounts of data.

Most importantly, we can only store string data in localStorage, which is vulnerable to XSS attacks, and it doesn’t offer much in the way of querying data.

This is the beauty of IndexedDB. Using IndexedDB, we can create structured databases in the browser, store almost everything in these databases, and perform various types of queries on the data.

In this article, we’ll learn all about IndexedDB and how to use Dexie.js (a minimalist wrapper for IndexedDB) to handle offline data storage in Web applications.

How does IndexedDB work

IndexedDB is a built-in non-relational database for browsers. It enables developers to persist data in the browser, making it seamless to use Web applications even when offline. When using IndexedDB, you will often see two terms: database storage and object storage. Let’s explore this below.

Create the database using IndexedDB

The IndexedDB database is unique to each Web application. This means that an application can only access data from IndexedDB databases that run in the same domain or subdomain as it does. A database is a place that houses object storage, which in turn contains stored data. To use IndexedDB databases, we need to open (or connect to) them:

const initializeDb = indexedDB.open('name_of_database', version)
Copy the code

The name_OF_DATABASE parameter in the indexeddb.open () method will be used as the name of the database being created, and the version parameter is a number representing the version of the database.

In IndexedDB, we use the object store to build the structure of the database, and each time we update the database structure, we need to upgrade the version to a higher value. This means that if we start with version 1, the next time we want to update the structure of the database, we need to change the version in the indexeddb.open () method to version 2 or higher.

Create object stores using IndexedDB

Object stores are similar to collections of tables in relational databases (such as PostgreSQL) and document databases (such as MongoDB). To create an object store in IndexedDB we need to call the onupgradenneeded () method from the previously declared initializeDb variable:

initializeDb.onupgradeneeded = () = > {
  const database = initializeDb.result
  database.createObjectStore('name_of_object_store', {autoIncrement: true})}Copy the code

In the code block above, we get the database from the InitializedB.Result property and create the object store using its createObjectStore() method. The second parameter {autoIncrement: True} tells IndexedDB to automatically supply/increment the IDS of the items in the object store.

I omitted other terms such as transactions and cursors because of the amount of work required to use the low-level IndexedDB API. That’s why we need Dexie.js, which is a minimalist wrapper for IndexedDB. Let’s take a look at how Dexie simplifies the whole process of creating databases, object stores, storing data, and querying data from databases.

Dexie is used to store data offline

Using Dexie, creating IndexedDB databases and object stores is easy:

const db = new Dexie('exampleDatabase')
db.version(1).stores({
  name_of_object_store: '++id, name, price'.name_of_another_object_store: '++id, title'
})
Copy the code

In the code block above, we create a new database named exampleDatabase and assign it as a value to the DB variable. We use the db.version(version_number).stores() method to create an object store for the database. The value stored by each object represents its structure. For example, when storing data in the first object store, we need to provide an object with attributes name and Price. The ++ ID option works like the {autoIncrement: True} argument we used when creating the object store.

Note that we need to install and import the Dexie package before we can use it in our application. We’ll see how to do this when we start building our demo project.

What we’re going to build

For our demo project, we will build a market list application using Dexie.js and React. Our users will be able to add items they intend to purchase to the marketplace list, remove them or mark them as purchased.

We’ll see how to use Dexie useLiveQuery Hook to monitor changes in the IndexedDB database and rerender the React component when the database is updated. Here’s what our application looks like:

Set up our app

First, we’ll use the GitHub template created for the structure and design of the application. Here’s a link to the template. Clicking the **Use this Template ** button will create a new repository for you using an existing template, which you can then clone and Use.

Alternatively, with GitHub CLI installed on your computer, you can run the following command to create a repository named market-list-app from the GitHub template of market List:

gh repo create market-list-app --template ebenezerdon/market-list-template
Copy the code

Once this is done, you can proceed to clone and open your new application in the code editor. Using a terminal to run the following commands in an application directory should install NPM dependencies and start a new application:

npm install && npm start
Copy the code

When you navigate to the local URL (usually http://localhost:3000) in the success message, you should be able to see the new React application. Your new application should look like this:

When you open the./ SRC/app.js file, you’ll notice that our application component contains only the JSX code for the Marketplace List application. We are styling the classes in the Materialize framework and include their CDN links in the./public/index.html file. Next, we’ll see how to create and manage data using Dexie.

Use Dexie to create our database

To use dexie. js for offline storage in our React application, we’ll start by running the following command on a terminal to install the Dexie and Dexie-React-hooks packages:

npm i -s dexie dexie-react-hooks
Copy the code

We will use the useLiveQuery hook in the Dexie-React-hooks package to monitor changes and re-render our React component when updating the IndexedDB database.

Let’s add the following import statements to our./ SRC/app.js file. This will import Dexie and useLiveQuery hooks:

import Dexie from 'dexie'
import { useLiveQuery } from "dexie-react-hooks";
Copy the code

Next, we’ll create a new database called MarketList and declare our object store items:

const db = new Dexie('MarketList');
db.version(1).stores(
  { items: "++id,name,price,itemHasBeenPurchased"})Copy the code

Our Items object store will expect an object with attributes name, Price, and itemHasBeenPurchased, and the ID will be provided by Dexie. When adding new data to the object store, we will use the default Boolean value false for the itemHasBeenPurchased property and then update it to true when we purchase items from the market list.

Dexie React hook

Let’s create a variable to store all of our items. We’ll use the useLiveQuery hook to get data from the Items object store and watch what happens so that when the Items object store is updated, our allItems variable will be updated and our component will be re-rendered with the new data. We will do this inside the App component:

const App = () = > {
  const allItems = useLiveQuery(() = > db.items.toArray(), []);
  if(! allItems)return null. }Copy the code

In the code block above, we create a variable named allItems and use the useLiveQuery hook as its value. The syntax of the useLiveQuery hook is similar to the React useEffect hook in that it expects a function and an array of its dependencies as arguments. Our function parameter returns a database query.

Here, we get all the data in the Items object store in array format. In the next line, we use a condition to tell our component that if the allItems variable is undefined, it means the query is still loading.

Add items to our database

Still in the App component, let’s create a function called addItemToDb that we’ll use to add items to the database. We call this function every time we click the “ADD ITEM” button. Keep in mind that our component is re-rendered every time we update the database.

.const addItemToDb = async event => {
    event.preventDefault()
    const name = document.querySelector('.item-name').value
    const price = document.querySelector('.item-price').value
    await db.items.add({
      name,
      price: Number(price),
      itemHasBeenPurchased: false})}...Copy the code

In the addItemToDb function, we get the item name and price value from the form input field, and then add the new item data to the item object store using the db.[name_of_object_store].add method. We also set the default value of the itemHasBeenPurchased property to false.

Delete items from our database

Now that we have the addItemToDb function, let’s create a function called removeItemFromDb to remove data from our commodity object store:

.const removeItemFromDb = async id => {
  await db.items.delete(id)
}
...
Copy the code

Update the items in our database

Next, we will create a function called markAsPurchased to mark the item as purchased. Our function is called with the primary key of the item as its first argument — id in this case, which it will use to query the database for the item we want to mark as a purchase. Once the commodity is acquired, it updates its markAsPurchased property to true:

.const markAsPurchased = async (id, event) => {
  if (event.target.checked) {
    await db.items.update(id, {itemHasBeenPurchased: true})}else {
    await db.items.update(id, {itemHasBeenPurchased: false}}})...Copy the code

In markAsPurchased, we use the Event parameter to get the specific input element that the user clicks. If its value is selected, we update the itemHasBeenPurchased property to true, otherwise to false. The db.[name_of_object_store].update() method expects the primary key of the project as its first argument and the new object data as its second argument.

Here’s what our App component should look like at this stage.

.const App = () = > {
  const allItems = useLiveQuery(() = > db.items.toArray(), []);
  if(! allItems)return null

  const addItemToDb = async event => {
    event.preventDefault()
    const name = document.querySelector('.item-name').value
    const price = document.querySelector('.item-price').value
    await db.items.add({ name, price, itemHasBeenPurchased: false})}const removeItemFromDb = async id => {
    await db.items.delete(id)
  }

  const markAsPurchased = async (id, event) => {
    if (event.target.checked) {
      await db.items.update(id, {itemHasBeenPurchased: true})}else {
      await db.items.update(id, {itemHasBeenPurchased: false}}})... }Copy the code

Now we create a variable named itemData to hold our JSX code for all of our itemData:

.const itemData = allItems.map(({ id, name, price, itemHasBeenPurchased }) = > (
  <div className="row" key={id}>
    <p className="col s5">
      <label>
        <input
          type="checkbox"
          checked={itemHasBeenPurchased}
          onChange={event= > markAsPurchased(id, event)}
        />
        <span className="black-text">{name}</span>
      </label>
    </p>
    <p className="col s5">${price}</p>
    <i onClick={()= > removeItemFromDb(id)} className="col s2 material-icons delete-button">
      delete
    </i>
  </div>))...Copy the code

In the itemData variable, we map all the items in the allItems data array and then get the attributes ID, name, Price, and itemHasBeenPurchased from each item object. We then proceed and replace the previously static data with the new dynamic value in the database.

Note that we also use the markAsPurchased and removeItemFromDb methods as click event listeners for the corresponding buttons. We will add the addItemToDb method to the form’s onSubmit event in the next code block.

With itemData ready, let’s update the App component’s return statement to the following JSX code:

.return (
  <div className="container">
    <h3 className="green-text center-align">Market List App</h3>
    <form className="add-item-form" onSubmit={event= > addItemToDb(event)} >
      <input type="text" className="item-name" placeholder="Name of item" required/>
      <input type="number" step=". 01" className="item-price" placeholder="Price in USD" required/>
      <button type="submit" className="waves-effect waves-light btn right">Add item</button>
    </form>
    {allItems.length > 0 &&
      <div className="card white darken-1">
        <div className="card-content">
          <form action="#">
            { itemData }
          </form>
        </div>
      </div>
    }
  </div>)...Copy the code

In the return statement, we have added the itemData variable to our items list. We also use the addItemToDb method as the onSubmit value of the Add-item-form.

To test our application, we can go back to the React page we opened earlier. Keep in mind that your React application must be running; if not, run the command NPM start on your terminal. Your application should work as shown below:

We can also use conditions to query our IndexedDB database with Dexie. For example, if we wanted to get all the items that cost more than $10, we could do the following:

const items = await db.friends
  .where('price').above(10)
  .toArray();
Copy the code

You can view other query methods in the Dexie documentation.

Closing remarks and source code

In this article, you learned how to use IndexedDB for offline storage and how Dexie.js simplifies the process. We also learned how to use the Dexie useLiveQuery hook to monitor changes and rerender the React component every time the database is updated.

Because IndexedDB is browser-native, querying and retrieving data from the database is much faster than sending server-side API requests every time we need to process data in our application, and we can store almost anything in an IndexedDB database.

Browser support with IndexedDB might have been a big issue in the past, but it is now supported by all major browsers. The advantages of using IndexedDB for offline storage in Web applications outweigh the disadvantages, and using Dexie.js with IndexedDB makes Web development more interesting than ever.

Here’s a link to the GitHub library for our demo application.

More articles

  • Rust and Python: Why can Rust replace Python
  • Use RoughViz to visualize sketch charts in vue.js
  • Programming calendar applets, applets for cloud development and generating sharing poster practices
  • Improve application performance and code quality: compose HTTP requests through proxy patterns
  • Object – oriented programming is the biggest mistake in computer science
  • Translation | make HTML 5 digital input only accept an integer
  • 13 top free WYSIWYG text editor tools