Handling reactivity with Dexie in Vue

If you’ve worked with Vue, you know how important reactivity is to building great apps. In this post, I’m going to cover how reactivity works with RxJS - reactivity library, Dexie - IndexedDB wrapper, and TypeScript.

A quick overview of the issue

You have a database with all the pretty CRUD functions for a table. Let’s say the table is a user table. You want to display a list of users in your database on a page. The great part is that you already have a function for that which returns all of your users. You visit the page and, boom, there’s all of the users in your database.

Now, what happens if you add a user to your database? Hold on, the user didn’t just magically appear on the page? Now you need to refresh the page to see the new user. I get it, refreshing isn’t an exhausting task. However, it’s still nice to see the user appear in the list with no extra work. Enter reactivity!

Observables and reactivity?

Vue has a hook called useObservable which uses an RxJS Observable to subscribe to events and returns a ref. What does that actually mean?

There are multiple parts to reactivity - the dependencies, the subscriber, and the side effect. The subscriber listens for changes in its dependencies. When the dependencies change or update, the side effect is what the subscriber produces. As seen in the Reactivity in Depth guide from Vue, imagine a function that has two dependencies A0 and A1. If the function added A0 and A1 together and returned A2 every time A0 or A1 were updated, A2 would be the side effect.

A RxJS Observable handles all of that reactivity behind the scenes for you and useObservable is more or less a Vue specific wrapper for this implementation.

Some pseudo code for our users table could look like this

import the getUsers function from our database
import the useObservable function from @vueuse/rxj

store the effect of useObservable in a variable called users
call the getUsers function in the useObservable
return the users

In the pseudo code above, the users variable will be updated when users are added to the database - always keeping a rolling record of users.'

What is liveQuery?

Dexie provides a function called liveQuery which turns a function into an Observable. liveQuery primarily handles querying the database when the database is updated. So, if you insert into the database, liveQuery knows this and passes the data back accordingly.

Creating an observable hook with Dexie

Firstly, I want to say a huge thanks to SD-Gaming in the Dexie issues on GitHub for this type safe hook! Let’s look at this hook from the top down.

First, we need to set 3 parameters in our generic useLiveQuery hook - the querier, the dependencies, and the options. The querier is the Dexie function that returns values from the database. The dependencies are anything that the querier function requires to run - it’s arguments. And the options are an optional object we can use to handle extra events like errors. This hook also returns a readonly ref of the generic type. This means that we cannot update the data via the hook, only read from it.

Inside the hook, we set the value which is a ref of the generic type, our observable is a liveQuery of the function we passed in as the querier, and the subscription is the observable subscribing to changes in our liveQuery. When the result of the liveQuery is updated, the value is then updated with the latest query results.

💡
Remember the section about reactivity above? Notice how similar this hook’s body is to the requirements of reactivity. We have a subscriber which is always updating when its dependencies are updated. So, when our liveQuery updates because the database has been updated, it returns an observable. The value variable here is the side effect of that observable.

We also use the Vue watch function to return a callback when the dependencies passed into the deps array change. If these values change, then the value variable’s contents are updated via our subscriber - the liveQuery.

When the hook is unmounted, we then unsubscribe from the liveQuery.

Finally, we return the value as a readonly ref of the generic type.

See it in action

I’ve created a simple project for tracking Phase 10 scores. My family plays Phase 10 so much that it was worth the effort to create an app to track the game scores instead of manually doing it on paper. In this app, I use Dexie to store the rounds and the users to persist the state of the game.

You’ll see in the useLiveQuery hook in src/utils/hooks/useLiveQuery.ts. Then the hook is actually used in the src/components/scoreSheet.vue file. Here, I track the players and the scores. Whenever either one is updated, the table of scores automatically gets a new row showing that round of Phase 10 scores and phases.