Build a basic NextJs app with IndexedDB and Dexie.js
localStorage Vs. IndexedDB
LocalStorage is a lightweight way to store key-value pairs. The API is very simple, but usage is capped at 5MB in many browsers. Plus the API is synchronous, so as we’ll see later, it can block the DOM. Browser support is very good.
- Key-value storage that stores values as strings
- Does not have expiration date (persistent storage) unless explicitly clear the browser using settings or Javascript
- Up to 10MB data can be stored
- Follow the same-origin policy, which means the Protocol(Http/Https), port and the host are the same. Only scripts of the same origin can access LocalStorge data
- Do not send to server, for client-side usage only
IndexedDB is a low-level Web API for storing large data structures within browsers and indexing them for high-performance searching. While Web Storage is useful for storing smaller amounts of data, it is less useful for storing larger amounts of structured data.
It is an alternative to the Web SQL (deprecated) database. It is a key-value pair NoSQL database and supports large scale storage (up to 20%–50% of hard drive capacity). It exposes an asynchronous API that supposedly avoids blocking the DOM. It supports many data types like number, string, JSON, blob, and so on.
- Can store both objects and key-value pairs
- Up to 250MB for IE
- IndexedDB API is asynchronous, unlike localStorage. IndexedDB operations are event-driven by various events like onsuccess, onerror, oncomplete etc.
- Follow the same-origin policy
- Do not have expiration time (persistent storage) unless explicit deletion
Use case example:
localStorage: Can be used to store user-related data
IndexedDB: When need to store a large number of objects which is time-consuming and a lag on performance to convert to string for Local Storage every time.
LocalStorage is useful for storing smaller amounts of data, it is less useful for storing larger amounts of structured data. So if you want to store significant amounts of structured data then IndexedDB is what you should choose. Another advantage of using IndexedDB is that you can do high performance searches using indexes.
Create a Next.js App
You can initialize a new Next.js application by calling the tool and supplying a name for your project:
npx create-next-app nextjs-with-indexed-db-dexie-js --typescript
npx will prompt you to install it automatically.
You currently find four main scripts listed in your package.json file:
"name": "nextjs-with-indexed-db-dexie-js",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "12.2.5",
"react": "18.2.0",
"react-dom": "18.2.0"
}
}
- dev – runs a development server on localhost:3000
- build – creates a built application ready for deployment
- start – starts your built Next application (must run next build first)
- lint – will "lint" your Next project using the dev dependency ESLint to warn you if your written code needs to be fixed
To run your Next project in development, make sure you are in your project folder (my-next-project) and run the dev script:
yarn run dev
After your project is up and running on localhost:3000, navigate there and you should see a default app:
Develop
Dexie.js
Dexie.js is a minimalist wrapper for IndexedDB. It is a straightforward approach to use IndexedDB with an easy integration process with several frontend frameworks.
Install Dexie.js
- Open Terminal
- Move into the nextjs-with-indexed-db-dexie-js project parent directory.
- install dexie.js and dexie-react-hooks
yarn add dexie dexie-react-hooks
Config database
Dexie instances are typically declared as modules in applications. This is where you specify which tables you require and how each one will be indexed. Throughout the application, a Dexie instance is a singleton; you do not need to create it on demand. Export the resulting db instance from your module so that other modules or components can query or write to the database.
Create a database config file: database.config.ts
// database.config.ts
import Dexie from "dexie";
const database = new Dexie("database");
database.version(1).stores({
customers: '++id, name, dept',
});
export const customerTable = database.table('customers');
export default database;
Create Customer type: types.ts
// types.ts
export interface ICustomer {
id: number;
name: string;
dept: number;
}
Create a component that adds data
Here we’re gonna create a simple React component (CustomerForm.tsx) that allows the user to add customers into the database using Table.add()
.
// CustomerForm.tsx
import type {FC} from 'react';
import {customerTable} from "../database/database.config";
import {ICustomer} from "../database/types";
const CustomerForm: FC = () => {
const createCustomer = async (event) => {
event.preventDefault()
const customer: ICustomer = {
name: event.target.name.value,
dept: Number(event.target.dept.value)
}
try {
// Add the new customer!
const id = await customerTable.add(customer);
console.info(`A new customer was created with id ${id}`);
event.target.reset()
} catch (error) {
console.error(`Failed to add ${customer}: ${error}`);
}
}
return <div>
<h1>Create customer</h1>
<form onSubmit={createCustomer}>
<label htmlFor="name">Name:</label><br />
<input type="text" id="name" name="name"/><br /><br />
<label htmlFor="dept">Dept:</label><br />
<input type="number" id="dept" name="dept"/><br /><br />
<button type="submit">Create</button>
</form>
</div>
};
export default CustomerForm;
see Dexie’s quick reference for more examples
Create a component that queries data
Write a simple component (CustomersList.tsx) that filter in the database by dept.
// CustomersList.tsx
import type {FC} from 'react';
import {useLiveQuery} from "dexie-react-hooks";
import {customerTable} from "../database/database.config";
import {useState} from "react";
const CustomersList: FC = () => {
const [lower, setLower] = useState(0);
const [upper, setUpper] = useState(10000);
const customer = useLiveQuery(
() => customerTable.where("dept").between(lower, upper).toArray(),
[lower, upper]
);
return <div>
<h1>Customers</h1>
lower: <input type="number" value={lower} onChange={(e) => setLower(Number(e.target.value))}/> & upper: <input type="number" value={upper} onChange={(e) => setUpper(Number(e.target.value))}/>
<ul>
{customer?.map(customer => <li key={customer.id}>
{customer.name}, {customer.dept}$
</li>)}
</ul></div>
}
export default CustomersList
see Dexie’s quick reference for more examples
Integrate components
Update index.tsx page
// index.tsx
import type {NextPage} from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import CustomerForm from "../components/CustomerForm";
import dynamic from "next/dynamic";
const CustomersList = dynamic(() =>import("../components/CustomersList"), {ssr: false})
const Home: NextPage = () => {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app"/>
<link rel="icon" href="/favicon.ico"/>
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to Build a basic NextJs app with IndexedDB and Dexie.js example
</h1>
<CustomerForm/>
<hr />
<CustomersList/>
</main>
</div>
)
}
export default Home
Let's show the result
Cool! Now we can change the order we see notes in! Now you’ve seen the basics of IndexedDB. There are other functionalities we didn’t see in action, like deleting objects, storing binary data in IndexedDB, and multi-field indices, but this should be a good starting point for building web apps with IndexedDB.