Dexie.liveQuery() — механизм реактивного выполнения
запросов в Dexie.js, который превращает обычные обращения к IndexedDB в
поток значений, автоматически обновляющийся при изменении данных. В
отличие от стандартных промисов, возвращающих результат один раз,
liveQuery создаёт подписку на состояние базы и
пересчитывает результат при каждом изменении затронутых таблиц.
Ключевая идея заключается в том, что результат запроса рассматривается как динамическое значение во времени, а не как одноразовый снимок.
Dexie.liveQuery() принимает функцию, внутри которой
выполняется любой чтение-ориентированный код Dexie. Эта функция
возвращает значение или промис, который резолвится в итоговый результат
запроса.
import Dexie fr om "dexie";
const db = new Dexie("app");
db.version(1).stores({
todos: "++id,title,done"
});
const query$ = Dexie.liveQuery(() => {
return db.todos.wh ere("done").equals(0).toArray();
});
query$ в этом примере — не обычный массив и не промис, а
Observable-подобный поток значений. При каждом изменении таблицы
todos результат автоматически пересчитывается.
Чтобы начать получать значения, используется подписка:
const subscription = query$.subscribe({
next: (value) => {
console.log("Список задач:", value);
},
error: (err) => {
console.error("Ошибка liveQuery:", err);
}
});
Подписка получает:
next — новый результат запросаerror — ошибку выполнения запросаКаждое изменение данных, влияющее на результат, приводит к повторному вызову функции запроса и эмиссии нового значения.
Dexie отслеживает зависимости запроса на уровне транзакций чтения.
Когда внутри liveQuery выполняется чтение из таблицы,
библиотека регистрирует, какие индексы и записи были затронуты.
При последующих изменениях данных Dexie:
Важно, что реактивность не требует ручного описания зависимостей.
Подписка должна быть явно завершена, чтобы избежать утечек памяти:
subscription.unsubscribe();
После вызова unsubscribe поток перестаёт реагировать на
изменения базы.
Если внутри функции запроса происходит ошибка, она передаётся в
error обработчик. Подписка при этом может завершиться или
продолжить работу в зависимости от типа ошибки и контекста
выполнения.
const q$ = Dexie.liveQuery(() => {
return db.todos.where("unknownIndex").toArray();
});
q$.subscribe({
next: console.log,
error: (e) => {
console.error("Запрос упал:", e);
}
});
Ошибки часто возникают при:
liveQuery поддерживает любые возвращаемые значения:
toArray)first, get)const count$ = Dexie.liveQuery(() => {
return db.todos.count();
});
Каждое изменение таблицы, влияющее на количество записей, приведёт к новому значению.
Функция внутри liveQuery может быть async,
что позволяет использовать await:
const enriched$ = Dexie.liveQuery(async () => {
const todos = await db.todos.toArray();
return todos.map(t => ({
...t,
priority: t.done ? 0 : 1
}));
});
Dexie корректно обрабатывает Promise и интегрирует его в поток значений.
Обновление происходит не по изменению конкретного поля, а по влиянию изменения на результат запроса. Это означает:
const activeSorted$ = Dexie.liveQuery(() => {
return db.todos
.where("done")
.equals(0)
.sortBy("title");
});
Любое добавление, удаление или обновление done или
title может привести к пересчёту результата.
liveQuery часто применяется как слой реактивных данных
для UI-фреймворков.
Пример интеграции с React-подобной моделью:
function useLiveTodos() {
const [state, setState] = useState([]);
useEffect(() => {
const sub = Dexie.liveQuery(() =>
db.todos.where("done").equals(0).toArray()
).subscribe(setState);
return () => sub.unsubscribe();
}, []);
return state;
}
Такой подход устраняет необходимость вручную синхронизировать состояние с IndexedDB.
Функция liveQuery выполняется внутри чтения базы данных.
Это означает:
Однако вложенные асинхронные операции могут выходить за пределы одной
транзакции, поэтому важно учитывать возможные изменения между
await-шагами.
При активном использовании liveQuery важно учитывать
частоту обновлений:
Оптимальный подход — минимизировать объём данных внутри
liveQuery и избегать тяжёлых преобразований.
Dexie не кэширует результат liveQuery в традиционном
смысле, но:
Таким образом, реактивность остаётся актуальной без полного пересчёта при каждом событии записи.
Один и тот же liveQuery можно использовать несколькими
подписчиками:
const q$ = Dexie.liveQuery(() => db.todos.toArray());
q$.subscribe(setA);
q$.subscribe(setB);
Dexie оптимизирует выполнение запроса, избегая дублирования работы при одинаковой логике.
Обычный запрос:
const todos = await db.todos.toArray();
выполняется один раз.
liveQuery:
Несмотря на удобство, существуют ограничения:
Можно комбинировать несколько потоков:
const active$ = Dexie.liveQuery(() =>
db.todos.where("done").equals(0).toArray()
);
const count$ = Dexie.liveQuery(() =>
db.todos.where("done").equals(0).count()
);
Каждый поток независим и реагирует на одни и те же изменения базы, но с разным результатом.
Типичный жизненный цикл включает:
liveQueryОтсутствие шага отмены приводит к накоплению активных слушателей, что критично в долгоживущих приложениях.
Эффективность liveQuery напрямую зависит от схемы
IndexedDB:
Проектирование схемы базы становится ключевым фактором производительности реактивных запросов.
При массовых операциях записи:
await db.transaction("rw", db.todos, async () => {
await db.todos.clear();
await db.todos.bulkAdd([...items]);
});
liveQuery может эмитить одно или несколько обновлений, в
зависимости от того, как Dexie агрегирует изменения внутри транзакции и
после её завершения.
Хотя возможно выполнять фильтрацию внутри liveQuery,
например:
Dexie.liveQuery(() =>
db.todos.toArray().then(list =>
list.filter(x => x.title.startsWith("A"))
)
);
такой подход менее эффективен, поскольку:
Каждое новое значение liveQuery возвращает новый объект
или массив. Это важно для UI:
liveQuery особенно эффективен в задачах:
Во всех случаях ключевым фактором является необходимость синхронизации UI с IndexedDB без ручного управления состоянием.