Dexie.js предоставляет реактивный слой поверх IndexedDB, но в связке
с Svelte особенно ценным становится сочетание liveQuery и
Svelte stores, позволяющее синхронизировать состояние базы данных с UI
без ручного управления подписками и императивных обновлений.
В основе подхода лежит разделение двух уровней реактивности:
liveQuery)Dexie.js предоставляет liveQuery, который превращает
запрос к IndexedDB в поток значений (Observable), пересчитывающийся при
изменениях данных. Svelte предоставляет store-модель, где любое
изменение состояния автоматически отражается в UI через подписки.
Ключевая идея интеграции — мост между Observable Dexie и store Svelte.
liveQuery оборачивает любой запрос Dexie в реактивный
поток:
import { liveQuery } fr om "dexie";
const todos$ = liveQuery(() =>
db.todos
.orderBy("createdAt")
.toArray()
);
Особенности поведения:
Важный момент: liveQuery не знает о Svelte напрямую, он
лишь предоставляет поток данных.
Svelte store ожидает интерфейс:
subscribe(run: (value) => void) => unsubscribeПоэтому требуется адаптер:
import { readable } fr om "svelte/store";
import { liveQuery } fr om "dexie";
function fromObservable(observable$, initialValue = null) {
return readable(initialValue, (set) => {
const subscription = observable$.subscribe({
next: set,
error: (err) => console.error(err)
});
return () => subscription.unsubscribe();
});
}
Теперь liveQuery становится store-источником:
const todos$ = fromObservable(
liveQuery(() => db.todos.toArray()),
[]
);
При масштабировании приложения появляется необходимость разделения:
export const todos = fromObservable(
liveQuery(() => db.todos.orderBy("createdAt").toArray()),
[]
);
Этот store всегда отражает текущее состояние IndexedDB.
Обновления выполняются через Dexie API, store не мутируется напрямую:
async function addTodo(text) {
await db.todos.add({
text,
done: false,
createdAt: Date.now()
});
}
После выполнения:
liveQuery автоматически пересчитывает результатНикаких ручных set() не требуется.
Svelte derived stores позволяют строить вычисления поверх Dexie-данных:
import { derived } from "svelte/store";
export const completedTodos = derived(
todos,
($todos) => $todos.filter(t => t.done)
);
Такая модель сохраняет:
Частая задача — реактивные запросы с параметрами:
import { writable, derived } from "svelte/store";
export const search = writable("");
export const filteredTodos = derived(
search,
($search, set) => {
const obs$ = liveQuery(() =>
db.todos
.wh ere("text")
.startsWithIgnoreCase($search)
.toArray()
);
const sub = obs$.subscribe(set);
return () => sub.unsubscribe();
}
);
Особенность: каждый новый параметр пересоздаёт подписку на
liveQuery.
При частых изменениях параметров важно избегать:
Подходы:
const queryCache = new Map();
function getTodosByPrefix(prefix) {
if (!queryCache.has(prefix)) {
queryCache.set(
prefix,
liveQuery(() =>
db.todos.wh ere("text").startsWith(prefix).toArray()
)
);
}
return queryCache.get(prefix);
}
const baseTodos$ = liveQuery(() => db.todos.toArray());
export const todos = fromObservable(baseTodos$, []);
Фильтрация переносится в Svelte слой.
Ошибки IndexedDB или Dexie транзакций должны корректно попадать в UI:
function fromObservable(observable$, initialValue = null) {
return readable(initialValue, (set) => {
const sub = observable$.subscribe({
next: set,
error: (error) => {
console.error("Dexie error:", error);
set(initialValue);
}
});
return () => sub.unsubscribe();
});
}
Расширенный вариант:
Сложные экраны требуют агрегации нескольких источников:
const todos$ = liveQuery(() => db.todos.toArray());
const stats$ = liveQuery(async () => ({
total: await db.todos.count(),
done: await db.todos.wh ere("done").equals(1).count()
}));
Svelte stores:
export const todos = fromObservable(todos$, []);
export const stats = fromObservable(stats$, { total: 0, done: 0 });
Dexie транзакции обеспечивают атомарность, но UI может требовать промежуточных состояний:
async function toggleTodo(id) {
await db.transaction("rw", db.todos, async () => {
const todo = await db.todos.get(id);
await db.todos.update(id, { done: !todo.done });
});
}
После завершения транзакции:
liveQuery фиксирует измененияПри использовании Svelte в SSR-сценариях возникает проблема:
liveQuery не может выполнятьсяРешение:
import { browser } fr om "$app/environment";
const todos$ = browser
? liveQuery(() => db.todos.toArray())
: {
subscribe: (run) => {
run([]);
return () => {};
}
};
Это позволяет:
Слой данных обычно структурируется так:
Пример разделения:
// queries.ts
export const allTodos$ = () => liveQuery(() => db.todos.toArray());
// stores.ts
export const todos = fromObservable(allTodos$(), []);
Dexie.js гарантирует:
liveQuery получает уже консистентное состояние, поэтому
Svelte store всегда отражает финальную версию данных без промежуточных
артефактов.
Важно понимать уровень обновлений:
Оптимизация:
Типичный pipeline:
IndexedDB → Dexie mutation → liveQuery invalidation → Observable emission → Svelte store update → UI rerender
Ключевая особенность — отсутствие ручных bridge-слоёв после инициализации.
При росте количества stores возникают проблемы:
Решение — централизованный query layer:
class TodoRepository {
all() {
return liveQuery(() => db.todos.toArray());
}
byStatus(done) {
return liveQuery(() =>
db.todos.wh ere("done").equals(done ? 1 : 0).toArray()
);
}
}
export const repo = new TodoRepository();
Stores строятся поверх репозитория, сохраняя единый источник истины.
UI события не влияют на store напрямую:
Такой цикл исключает: