Реактивный слой поверх IndexedDB в Dexie.js строится вокруг идеи автоматического отслеживания зависимостей запроса и пересчёта результата при изменении данных. В связке с React этот механизм позволяет отказаться от ручного управления состоянием для большинства сценариев чтения данных из локальной базы.
useLiveQuery выступает мостом между реактивной моделью
Dexie и жизненным циклом React-компонента, обеспечивая подписку на
изменения IndexedDB и синхронизацию результата с рендером.
useLiveQueryХук принимает функцию запроса, внутри которой выполняются операции чтения Dexie-таблиц. Эта функция выполняется один раз при монтировании компонента и повторно — при любом изменении зависимостей данных, обнаруженных Dexie.
Сигнатура:
const result = useLiveQuery(queryFn, deps?, defaultValue?);
queryFn — функция, возвращающая Promise или синхронное
значениеdeps — массив зависимостей React (опционально)defaultValue — начальное значение до завершения первого
запросаКлючевой момент: зависимость отслеживается не через React, а через внутренний механизм Dexie, основанный на наблюдении за чтением таблиц и индексов.
При выполнении queryFn Dexie регистрирует все обращения
к таблицам:
table.get()where()equals, above,
betweentoArray()Каждый такой доступ добавляется в граф зависимостей. Когда происходит изменение данных (добавление, удаление, обновление), Dexie проверяет, затрагивает ли изменение активные подписки.
Если затрагивает — useLiveQuery инициирует повторный
запуск queryFn, а React получает новый результат через
setState внутри хука.
Типичный пример:
import { useLiveQuery } fr om "dexie-react-hooks";
import { db } fr om "./db";
function TasksList() {
const tasks = useLiveQuery(
() => db.tasks.toArray(),
[]
);
if (!tasks) return <div>Loading...</div>;
return (
<ul>
{tasks.map(t => (
<li key={t.id}>{t.title}</li>
))}
</ul>
);
}
В этом примере:
taskstoArray()Реактивность сохраняется при использовании индексов:
const completed = useLiveQuery(
() => db.tasks.wh ere("done").equals(1).toArray(),
[]
);
Dexie отслеживает не только таблицу tasks, но и
конкретный индекс done. Это означает, что обновление любого
поля done в записи приведёт к пересчёту результата.
Второй аргумент deps работает независимо от реактивного
слоя Dexie:
const userTasks = useLiveQuery(
() => db.tasks.where("userId").equals(userId).toArray(),
[userId]
);
Здесь:
userId инициирует новый запрос через
ReactТаким образом, существует два уровня триггеров:
queryFn может возвращать Promise:
const stats = useLiveQuery(async () => {
const count = await db.tasks.count();
const completed = await db.tasks.where("done").equals(1).count();
return { count, completed };
}, []);
Dexie корректно обрабатывает асинхронные функции, сохраняя реактивную подписку на все задействованные таблицы.
defaultValueДо завершения первого выполнения queryFn состояние равно
undefined. Для контроля UI используется
defaultValue:
const tasks = useLiveQuery(
() => db.tasks.toArray(),
[],
[]
);
Это позволяет избежать дополнительных проверок на null в
JSX и упрощает рендеринг списков.
Если внутри queryFn возникает ошибка:
undefinedОбработка ошибок обычно реализуется через try/catch:
const data = useLiveQuery(async () => {
try {
return await db.tasks.toArray();
} catch (e) {
return [];
}
}, []);
Dexie использует дифференциальное отслеживание:
Это снижает нагрузку при масштабировании интерфейса.
const tasks = useLiveQuery(
() => db.tasks
.where("priority")
.aboveOrEqual(2)
.toArray(),
[]
);
const summary = useLiveQuery(async () => {
const total = await db.tasks.count();
const done = await db.tasks.where("done").equals(1).count();
return {
total,
done,
percent: total ? done / total : 0
};
}, []);
const data = useLiveQuery(async () => {
const tasks = await db.tasks.toArray();
const users = await db.users.toArray();
return { tasks, users };
}, []);
Запросы внутри useLiveQuery могут выполняться в
транзакционном контексте:
const result = useLiveQuery(() =>
db.transaction("r", db.tasks, db.users, async () => {
const tasks = await db.tasks.toArray();
const users = await db.users.toArray();
return { tasks, users };
})
, []);
Это гарантирует консистентность данных на момент чтения.
Ключевые аспекты:
toArray() при больших таблицахuseLiveQueryПример оптимизации:
const taskIds = useLiveQuery(
() => db.tasks.orderBy("updatedAt").keys(),
[]
);
Функция queryFn должна быть стабильной для
предотвращения лишних пересчётов при React-рендерах:
const query = useCallback(
() => db.tasks.where("done").equals(0).toArray(),
[]
);
const tasks = useLiveQuery(query, []);
Хотя Dexie сам управляет подписками, React-референсы функции влияют на жизненный цикл подписки.
useLiveQuery не рассчитан на серверный рендеринг:
undefinedconst tasks = typeof window !== "undefined"
? useLiveQuery(() => db.tasks.toArray(), [])
: [];
Тип результата выводится автоматически:
interface Task {
id: number;
title: string;
done: boolean;
}
const tasks = useLiveQuery<Task[]>(() => db.tasks.toArray(), []);
Для сложных структур предпочтительно явно задавать тип возвращаемого значения функции запроса.
Результаты useLiveQuery могут использоваться для
построения производных данных:
const tasks = useLiveQuery(() => db.tasks.toArray(), []);
const grouped = useMemo(() => {
if (!tasks) return {};
return tasks.reduce((acc, t) => {
acc[t.done ? "done" : "open"] ??= [];
acc[t.done ? "done" : "open"].push(t);
return acc;
}, {});
}, [tasks]);
Такой подход отделяет реактивный слой данных от UI-логики трансформации.
При интенсивных изменениях данных возможны частые пересчёты. Используются стратегии:
queryFnЛюбая операция записи автоматически триггерит обновление подписок:
await db.tasks.add({
title: "New task",
done: false
});
После выполнения:
useLiveQuery пересчитываютсяconst dashboard = useLiveQuery(async () => {
const [tasks, users, logs] = await Promise.all([
db.tasks.toArray(),
db.users.toArray(),
db.logs.lim it(50).toArray()
]);
return { tasks, users, logs };
}, []);
Все используемые таблицы автоматически становятся частью подписки,
включая ограничения (limit, where и
индексы).