Использование с React: хук useLiveQuery

Реактивный слой поверх 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, between
  • полные сканы toArray()

Каждый такой доступ добавляется в граф зависимостей. Когда происходит изменение данных (добавление, удаление, обновление), 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>
  );
}

В этом примере:

  • запрос подписывается на таблицу tasks
  • любое изменение таблицы вызывает повторный toArray()
  • React автоматически обновляет UI

Работа с фильтрацией и индексами

Реактивность сохраняется при использовании индексов:

const completed = useLiveQuery(
  () => db.tasks.wh ere("done").equals(1).toArray(),
  []
);

Dexie отслеживает не только таблицу tasks, но и конкретный индекс done. Это означает, что обновление любого поля done в записи приведёт к пересчёту результата.


Зависимости React и их роль

Второй аргумент deps работает независимо от реактивного слоя Dexie:

const userTasks = useLiveQuery(
  () => db.tasks.where("userId").equals(userId).toArray(),
  [userId]
);

Здесь:

  • изменение userId инициирует новый запрос через React
  • изменение данных в IndexedDB — через Dexie

Таким образом, существует два уровня триггеров:

  • React dependencies — управляют параметрами запроса
  • Dexie subscriptions — отслеживают изменения данных

Асинхронные запросы

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 использует дифференциальное отслеживание:

  • изменение нерелевантных таблиц не вызывает пересчёт
  • повторный запуск происходит только при изменении наблюдаемых индексов
  • одинаковые запросы к разным компонентам разделяют подписку

Это снижает нагрузку при масштабировании интерфейса.


Частые паттерны использования

1. Список с фильтрацией

const tasks = useLiveQuery(
  () => db.tasks
    .where("priority")
    .aboveOrEqual(2)
    .toArray(),
  []
);

2. Агрегаты

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
  };
}, []);

3. Связанные таблицы

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-референсы функции влияют на жизненный цикл подписки.


SSR и гидратация

useLiveQuery не рассчитан на серверный рендеринг:

  • IndexedDB недоступна на сервере
  • результат на SSR всегда undefined
  • требуется fallback-значение или условное выполнение
const tasks = typeof window !== "undefined"
  ? useLiveQuery(() => db.tasks.toArray(), [])
  : [];

Типизация в TypeScript

Тип результата выводится автоматически:

interface Task {
  id: number;
  title: string;
  done: boolean;
}

const tasks = useLiveQuery<Task[]>(() => db.tasks.toArray(), []);

Для сложных структур предпочтительно явно задавать тип возвращаемого значения функции запроса.


Derived queries и композиция

Результаты 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
});

После выполнения:

  • Dexie определяет затронутые индексы
  • подписанные useLiveQuery пересчитываются
  • React получает обновлённое состояние без ручного вмешательства

Ограничения модели

  • отсутствие контроля над точным моментом пересчёта
  • невозможность частичного обновления результата
  • зависимость от структуры запросов Dexie
  • необходимость аккуратной работы с тяжёлыми выборками

Комбинирование нескольких источников данных

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 и индексы).