206 lines
9.0 KiB
Markdown
206 lines
9.0 KiB
Markdown
# Svelte and Dexie.js Integration Guide
|
|
|
|
This document provides a guide to integrating Svelte (with a focus on Runes) and Dexie.js for building reactive web applications. It covers key concepts and best practices for managing reactivity between Svelte components and the Dexie.js database.
|
|
|
|
## Svelte 5 Migration Guide
|
|
|
|
Svelte 5 introduces "runes" as a new way to manage reactivity. This is a major change from previous versions of Svelte, and it's important to understand the breaking changes before migrating.
|
|
|
|
### Key Breaking Changes
|
|
|
|
- **`let` is no longer reactive:** In Svelte 4, any `let` variable declared in the top-level scope of a component was automatically reactive. In Svelte 5, you must explicitly declare reactive state using the `$state` rune.
|
|
- **`$:` is replaced by `$derived` and `$effect`:** The `$` label is no longer used for reactive statements. Instead, you should use the `$derived` rune for computed values and the `$effect` rune for side effects.
|
|
- **`export let` is replaced by `$props`:** Component props are now declared using the `$props` rune, which provides a more flexible and explicit way to define component APIs.
|
|
- **Event handling:** The `on:` directive is replaced by event attributes (e.g., `onclick`). Component events are now handled using callback props instead of `createEventDispatcher`.
|
|
- **Slots are replaced by snippets:** The `<slot>` element is replaced by the `{#snippet ...}` block, which provides a more powerful and flexible way to pass content to components.
|
|
|
|
For a complete list of breaking changes, refer to the [Svelte 5 migration guide](https://svelte.dev/docs/svelte/v5-migration-guide).
|
|
|
|
## Dexie.js Quick Reference
|
|
|
|
Dexie.js is a lightweight, minimalistic wrapper for IndexedDB that makes it easier to work with client-side databases.
|
|
|
|
### Key Classes and Methods
|
|
|
|
- **`Dexie`:** The main class for creating and managing IndexedDB databases.
|
|
- `new Dexie(databaseName)`: Creates a new database instance.
|
|
- `version(versionNumber).stores({ ... })`: Defines the database schema.
|
|
- **`Table`:** Represents an object store (table) in the database.
|
|
- `add(item)`: Adds a new item to the table.
|
|
- `put(item)`: Adds or updates an item in the table.
|
|
- `update(key, changes)`: Updates an existing item.
|
|
- `delete(key)`: Deletes an item by its primary key.
|
|
- `get(key)`: Retrieves an item by its primary key.
|
|
- `where(index)`: Starts a query using an index.
|
|
- `toArray()`: Retrieves all items from the table as an array.
|
|
- **`Collection`:** Represents a collection of items resulting from a query.
|
|
- `toArray()`: Retrieves all items in the collection as an array.
|
|
- `first()`: Retrieves the first item in the collection.
|
|
- `last()`: Retrieves the last item in the collection.
|
|
- `each(callback)`: Iterates over each item in the collection.
|
|
- `modify(changes)`: Updates all items in the collection.
|
|
- `delete()`: Deletes all items in the collection.
|
|
|
|
For a complete list of API methods, refer to the [Dexie.js API Reference](https://dexie.org/docs/API-Reference).
|
|
|
|
## Integrating Svelte Runes and Dexie.js
|
|
|
|
The combination of Svelte Runes and Dexie.js allows for the creation of highly reactive and efficient web applications.
|
|
|
|
### The `liveQuery` Function
|
|
|
|
Dexie.js provides a `liveQuery` function that returns an observable of the query result. This observable can be used to automatically update the UI whenever the data in the database changes.
|
|
|
|
### Using `liveQuery` with Svelte Runes
|
|
|
|
To use `liveQuery` with Svelte Runes, you can create a custom readable store that wraps the `liveQuery` observable. This store can then be used in your Svelte components to display and interact with the data.
|
|
|
|
**1. Create a `liveQuery` store:**
|
|
|
|
```typescript
|
|
import { liveQuery } from 'dexie';
|
|
import { readable } from 'svelte/store';
|
|
import { db } from './db'; // Your Dexie database instance
|
|
|
|
export function createLiveQueryStore<T>(query: () => T | Promise<T>) {
|
|
return readable<T | undefined>(undefined, (set) => {
|
|
const subscription = liveQuery(query).subscribe({
|
|
next: (result) => set(result),
|
|
error: (error) => console.error(error)
|
|
});
|
|
return () => subscription.unsubscribe();
|
|
});
|
|
}
|
|
```
|
|
|
|
**2. Use the `createLiveQueryStore` in your component:**
|
|
|
|
```html
|
|
<script>
|
|
import { createLiveQueryStore } from './stores';
|
|
import { db } from './db';
|
|
|
|
const friends = createLiveQueryStore(() => db.friends.toArray());
|
|
</script>
|
|
|
|
<ul>
|
|
{#if $friends} {#each $friends as friend}
|
|
<li>{friend.name}</li>
|
|
{/each} {/if}
|
|
</ul>
|
|
```
|
|
|
|
The `createLiveQueryStore` function creates a readable store that automatically updates whenever the data in the `friends` table changes. The `$friends` variable in the component will always contain the latest data from the database.
|
|
|
|
## Page Load Strategies (Avoiding the "Waterfall")
|
|
|
|
When loading data for a primary page view (e.g., viewing a specific Journal, Session, or Person), you must choose a synchronization strategy to ensure the UI renders correctly on the first load.
|
|
|
|
### ❌ The "Fire & Forget" Anti-Pattern (AVOID)
|
|
Triggering a background load in `+page.ts` without `await` leads to race conditions.
|
|
1. `+page.svelte` mounts immediately.
|
|
2. `liveQuery` runs against an empty IndexedDB.
|
|
3. API data arrives later and writes to IndexedDB.
|
|
4. **Failure:** Svelte 5 + Dexie `liveQuery` may not automatically detect this first "cold start" update without a manual refresh.
|
|
|
|
### ✅ The "Blocking Loader" Pattern (RECOMMENDED)
|
|
Ensure the data is in IndexedDB **before** the component mounts.
|
|
1. In `+page.ts`, `await` the API load function.
|
|
2. In `+page.svelte`, the `liveQuery` will see the data immediately upon mount.
|
|
|
|
**Example (+page.ts):**
|
|
```typescript
|
|
export async function load({ params }) {
|
|
// Blocking await ensures IDB is populated
|
|
await journals_func.load_ae_obj_id__journal({
|
|
journal_id: params.journal_id,
|
|
try_cache: true
|
|
});
|
|
return {};
|
|
}
|
|
```
|
|
|
|
### ✅ The "Hydrate & Subscribe" Pattern (ADVANCED)
|
|
If you must use non-blocking loads, you must pass the initial data to the component to "hydrate" the state before the subscription takes over.
|
|
|
|
1. In `+page.ts`, `await` the load and **return the object**.
|
|
2. In `+page.svelte`, use the returned object as a fallback or initial state.
|
|
|
|
**Example (+page.svelte):**
|
|
```svelte
|
|
<script>
|
|
let { data } = $props();
|
|
let lq__obj = $derived(liveQuery(async () => db.table.get(id)));
|
|
</script>
|
|
|
|
<!-- Use fallback to handle the gap before liveQuery emits -->
|
|
{#if $lq__obj || data.initial_obj}
|
|
<View object={$lq__obj ?? data.initial_obj} />
|
|
{/if}
|
|
```
|
|
|
|
## Svelte 5 Binding Pitfalls
|
|
|
|
### 1. `props_invalid_value` (The "Expression Binding" Error)
|
|
Svelte 5's `bind:` directive is more restrictive than previous versions. You can only bind to a simple **Identifier** or **MemberExpression**.
|
|
|
|
**❌ Invalid Pattern (Causes Compile Error):**
|
|
Attempting to normalize a value *inside* the binding will fail.
|
|
```svelte
|
|
<!-- Error: Can only bind to an Identifier or MemberExpression -->
|
|
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id || null} />
|
|
```
|
|
|
|
**✅ Correct Pattern:**
|
|
Ensure the source value is already normalized before binding, or use a reactive effect to handle the fallback.
|
|
```typescript
|
|
// Normalize in an effect or derivation
|
|
$effect(() => {
|
|
if ($events_slct.event_session_id === undefined) {
|
|
$events_slct.event_session_id = null;
|
|
}
|
|
});
|
|
```
|
|
```svelte
|
|
<!-- Bind directly to the normalized property -->
|
|
<Launcher_menu bind:slct__event_session_id={$events_slct.event_session_id} />
|
|
```
|
|
|
|
---
|
|
|
|
## Safe Data Processing for IndexedDB Sorting
|
|
|
|
When preparing data for IndexedDB, especially when creating composite sort keys, it is critical to handle `null` or `undefined` values safely to prevent runtime crashes that can interrupt the data synchronization process.
|
|
|
|
### 1. Safe String Padding
|
|
Attempting to call `.toString()` or `.padStart()` on a `null` or `undefined` value will throw a `TypeError`. This is a common pitfall when processing optional fields like `sort` or `group`.
|
|
|
|
**Bad Pattern (Crash Risk):**
|
|
```typescript
|
|
// Crashes if obj.sort is null or undefined
|
|
obj.tmp_sort_1 = `${obj.sort.toString().padStart(3, '0')}`;
|
|
obj.tmp_sort_2 = `${obj.sort?.toString().padStart(3, '0') ?? ''}`; // Still risky if chaining is misunderstood
|
|
```
|
|
|
|
**Good Pattern (Safe):**
|
|
```typescript
|
|
// Safely handle null/undefined by defaulting to 0 or an empty string BEFORE string manipulation
|
|
const sort_val = (obj.sort ?? 0).toString().padStart(3, '0');
|
|
```
|
|
|
|
### 2. Correct Sorting with Dexie
|
|
Dexie's `sortBy()` method returns a new array sorted by the specified key. It **ignores** previous `reverse()` calls on the collection. To achieve a descending sort, you must sort first and then reverse the resulting array.
|
|
|
|
**Incorrect (Ascending Sort Result):**
|
|
```typescript
|
|
// .reverse() is ignored by .sortBy()
|
|
let results = await db.table.where('id').equals(id).reverse().sortBy('sort_key');
|
|
```
|
|
|
|
**Correct (Descending Sort Result):**
|
|
```typescript
|
|
// Sort ascending first, then reverse the array
|
|
let results = await db.table.where('id').equals(id).sortBy('sort_key');
|
|
return results.reverse();
|
|
```
|