Last active
July 29, 2025 22:50
-
-
Save jsteiner/08e4b4032d6aba2eb2bc29607e363f30 to your computer and use it in GitHub Desktop.
drizzle useLiveQuery with joins
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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 []; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.