Categories
Javascript Programming Svelte

Svelte stores in IndexedDB

I know that I just wrote a post about using Svelte Stores with localStorage but very shortly after writing that (and implementing it in my own app 🤦), I came across this blog post from Paul Maneesilasan which explained the benefits of using IndexedDB:

So, the primary benefits to using a datastore like indexedDB are: larger data storage limits (50MB), non-blocking operations, and the ability to do db operations beyond simple read/writes. In my case, the first two alone are enough to switch over.

https://www.paultman.com/from-localstorage-to-indexeddb/

Since I had some fresh ideas for my own app about storing lots of data that could potentially would definitely go over the Web Storage 5-10MB limit, I dug even deeper into the research. In the end, I too decided to make the switch.

Sexy Dexie

The documentation for IndexedDB can be overwhelming. It’s great how thorough it is, but I found it difficult to read. After spending a few hours trying to understand the basics, getting bored, looking at memes, and finally remembering what I was supposed to be doing, I came across a package called Dexie. Dexie is a wrapper for IndexedDB, which means that it will make calls to the database for me; for instance Table.add(item) will insert the item object on the specified table. This greatly reduced my need to build that functionality myself. Since I hate reinventing the wheel (and my brain can only learn so many things in a day), I opted to use this package to manage the database. Plus, Dexie has a handy tutorial for integrating with Svelte and makes managing database versions a breeze.

While the Dexie + Svelte tutorial is great, including Dexie in each and every component that gets and sets data (like the tutorial does) would be cumbersome. It would be far simpler to use Svelte stores across components, and have the stores talk to IndexedDB via Dexie.

Custom Stores

Since IndexedDB; and therefore Dexie, utilize asynchronous APIs, a few things need to be done differently to integrate with Svelte stores. Firstly, we can’t just “get the values” from the database and assign them to a writable store (like we did in the localStorage example) since we’ll be receiving a Promise from Dexie. Secondly, when the data is updated in the store, we also need to update the data in the database; asynchronously.

This is where custom stores come in. In a custom store, we can create our very own methods to manage getting and setting data. The catch is that the store must implement a subscribe() method. If it needs to be a writable store, then it also needs to implement a set() method. The set() method will be where the magic happens.

To get started, follow Dexie’s installation instructions (npm install dexie) and create a db.js file to initialize the database. I’ve added a couple methods to mine to keep database functionality organized:

Note that is generic code sampled from helth app so references to water, calories, and sodium are done in the context of a health tracker.

import { Dexie } from 'dexie';
// If using SvelteKit, you'll need to be 
// certain your code is only running in the browser
import { browser } from '$app/environment';

export const db = new Dexie('helthdb');

db.version(1).stores({
  journal: 'date, water, calories, protein, sodium',
  settings: 'name, value', 
});

db.open().then((db) => {
  // custom logic to initialize DB here
  // insert default values
};

export const updateLatestDay = (date, changes) => {
  if(browser) {
    return db.journal.update(date, changes);
  }
  return {};
};

export const getLatestDay = () => {
  if(browser) {
    return db.journal.orderBy('date').reverse().first();
  }
  return {};
};

export const updateItems = (tableName, items) => {
  if(browser) {
    // .table() allows specifying table name to perform operation on
    return db.table(tableName).bulkPut(items);
  }
  return {};
}

export const getItems = (tableName) => {
  // spread all of the settings records onto one object
  // so the app can use a single store for all settings
  // this table has limited entries
  // would not recommend for use with large tables
  if(browser) {
    return db.table(tableName).toArray()
    .then(data => data.reduce((prev, curr) => ({...prev, [curr.name]: curr}), []));
    // credit to Jimmy Hogoboom for this fun reducer function
    // https://github.com/jimmyhogoboom
  }
  return {};
}

I won’t go over this file because it’s self explanatory and yours will likely be very different. After you have a database, create a file for the stores (stores.js).

import * as dbfun from '$stores/db';
import { writable } from 'svelte/store';

// sourced from https://stackoverflow.com/q/69500584/759563
function createTodayStore() {

  const store = writable({});

  return {
    ...store,
    init: async () => {
      const latestDay = dbfun.getLatestDay();
      latestDay.then(day => {
        store.set(day);
      })
      return latestDay;
    },
    set: async (newVal) => {
      dbfun.getLatestDay()
        .then(day => {
          dbfun.updateLatestDay(day.date, newVal);
        });
      store.set(newVal);
    }
  }
}

function createNameValueStore(tableName) {
  const store = writable({});

  return {
    ...store,
    init: async () => {
      const items = dbfun.getItems(tableName);
      items.then(values => {
        store.set(values);
      })
      return items;
    },
    set: async (newVal) => {
      dbfun.updateItems(tableName, Object.keys(newVal).map((key) => {
        return {name: key, value: newVal[key].value}
      }));
      store.set(newVal);
    }
  }
}
export const today = createTodayStore();
export const settings = createNameValueStore('settings');

Here’s break down what this does:

  1. Import the db.js file and any extra functionality added to it.
  2. Import { writable } from svelte/store since we’ve already established that I hate reinventing the wheel.
  3. Create 2 functions; createTodayStore() and createNameValueStore(). Both functions are mostly identical except for the logic they call on the database.
  4. Each function then creates their own store from writable with an empty object as the default value.
  5. Both functions return an object that adheres to the store contract of Svelte by implementing all the functionality of writable, overriding set(), and adding a custom method init().
  6. init() queries the database asynchronously, sets the value of the store accordingly, then returns the Promise received from Dexie.
  7. set() updates the data within the database given to the store and then proceeds to set the store accordingly.
  8. Export the stores.

Accessing the stores

Now that the stores have been created, accessing them isn’t quite as simple as it was when they were synchronous. With a completely synchronous store, svelte allowed access to the data via the $ operator. The stores from localStorage could be accessed or bound simply by dropping $storeName wherever that data was needed. With the new asynchronous stores, calling the custom init()method is mandatory before accessing the data; otherwise, the store won’t have any data! Here’s a simple Svelte component showing how to access the settings store data in the <script> tag and binding to the today store in the markup.

<script>
  import { onMount, afterUpdate } from 'svelte';
  import { today, settings } from '$stores/stores';
  import Spinner from '$components/Spinner.svelte';

  $: reactiveString = 'loading...';

  onMount(() => {
    settings.init()
    .then(() => {
      if('water' in $settings) {
        reactiveString = `water set to ${settings.water.value}`;
      }
    });
</script>

{#await today.init()}
  <Spinner />
{:then}
  <input type="number" bind:value={$today.water} />
{:catch error}
  <p>error</p>
{/await}
<p>{reactiveString}</p>
  • <Spinner /> source can be found here.
  • This instance of a reactive variable doesn’t make much sense unless it were tied to another value but the logic remains the same.
  • Accessing the today store value in the markup is made possible by use of Svelte’s {#await}.

el fin

Is this the best way connect Svelte stores to IndexedDB?

I have no idea. Probably not.

But it does seem to work for me. If you’ve got ideas on how it could be improved, leave a comment. I’m always open to constructive criticism. If you’re curious about the app this is taken from, I’ve previously written about it here. The source code for it can be found on GitHub.

And lastly, if you’ve enjoyed this post or found value in it, consider sponsoring me on GitHub.

By Dylan Hildenbrand

10+ years experience in web development, homelab enthusiast, tinkerer

profile for Dylan Hildenbrand on Stack Exchange, a network of free, community-driven Q&A sites  

Do you like these posts? Consider sponsoring me or making a donation!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.