Skip to content

Instantly share code, notes, and snippets.

@jsteiner
Last active July 29, 2025 22:50
Show Gist options
  • Save jsteiner/08e4b4032d6aba2eb2bc29607e363f30 to your computer and use it in GitHub Desktop.
Save jsteiner/08e4b4032d6aba2eb2bc29607e363f30 to your computer and use it in GitHub Desktop.
drizzle useLiveQuery with joins
// @ts-nocheck
import { addDatabaseChangeListener } from "expo-sqlite";
import { useEffect, useState } from "react";
import { is } from "drizzle-orm/entity";
import { SQL } from "drizzle-orm/sql/sql";
import type { AnySQLiteSelect } from "drizzle-orm/sqlite-core";
import {
getTableConfig,
getViewConfig,
SQLiteTable,
SQLiteView,
} from "drizzle-orm/sqlite-core";
import { SQLiteRelationalQuery } from "drizzle-orm/sqlite-core/query-builders/query";
import { Subquery } from "drizzle-orm/subquery";
export const useLiveQuery = <
T extends
| Pick<AnySQLiteSelect, "_" | "then">
| SQLiteRelationalQuery<"sync", unknown>,
>(
query: T,
deps: unknown[] = [],
) => {
const [data, setData] = useState<Awaited<T>>(
(is(query, SQLiteRelationalQuery) && query.mode === "first"
? undefined
: []) as Awaited<T>,
);
const [error, setError] = useState<Error>();
const [updatedAt, setUpdatedAt] = useState<Date>();
useEffect(() => {
const entity = is(query, SQLiteRelationalQuery)
? query.table
: (query as AnySQLiteSelect).config.table;
if (is(entity, Subquery) || is(entity, SQL)) {
setError(
new Error(
"Selecting from subqueries and SQL are not supported in useLiveQuery",
),
);
return;
}
let listener: ReturnType<typeof addDatabaseChangeListener> | undefined;
const handleData = (data: any) => {
setData(data);
setUpdatedAt(new Date());
};
query.then(handleData).catch(setError);
if (is(entity, SQLiteTable) || is(entity, SQLiteView)) {
const config = is(entity, SQLiteTable)
? getTableConfig(entity)
: getViewConfig(entity);
const relationTableNames = getRelationTableNames(query);
const listeningTables = [config.name, ...relationTableNames];
listener = addDatabaseChangeListener(({ tableName }) => {
if (listeningTables.includes(tableName)) {
query.then(handleData).catch(setError);
}
});
}
return () => {
listener?.remove();
};
}, deps); // eslint-disable-line react-hooks/exhaustive-deps
return {
data,
error,
updatedAt,
} as const;
};
const getRelationTableNames = (query: SQLiteRelationalQuery) => {
if (query.config.with) {
return Object.keys(query.config.with).map(
(relation) => query.tableConfig.relations[relation].referencedTableName,
);
} else if (query.config.joins) {
return query.config.joins.map(
(join) => join.table[Symbol.for("drizzle:BaseName")],
);
} else {
return [];
}
};
@jsteiner
Copy link
Author

jsteiner commented Jul 29, 2025

This was my solution for this PR.

FWIW, I wrote this for a toy app, and I didn't spend any time trying to understand what drizzle is doing. I just pattern matched to get it working, so I'd be careful using it in production applications. There are likely edge cases I haven't considered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment