There is still a problem with the current implementation of useQuery: type-safety.
The return type of useLiveQuery is untyped. We need to decode the data to make sure it matches the expected type.
We do this using Schema from effect. For each query, we define a new Schema responsible for decoding the data.
We add a generic Schema parameter to useQuery to validate the data:
export const useQuery = <A, I>(
query: (orm: ReturnType<typeof usePgliteDrizzle>) => Query,
schema: Schema.Schema<A, I>
) => {
const orm = usePgliteDrizzle();
const { params, sql } = query(orm);
// 👇 Mark return type as `I` (encoded data)
return useLiveQuery<I>(sql, params);
};Since useLiveQuery returns a list of values, we need to extract them and use Schema.decode:
- If the data is
undefined, it means the query has not been executed yet and we return aMissingDataerror - If decoding fails, it means the data is invalid and we return a
InvalidDataerror
We wrap the
schemaparameter insideSchema.Array. We also usedecodeEitherto handle the error usingEither.
export const useQuery = <A, I>(
query: (orm: ReturnType<typeof usePgliteDrizzle>) => Query,
schema: Schema.Schema<A, I>
) => {
const orm = usePgliteDrizzle();
const { params, sql } = query(orm);
const results = useLiveQuery<I>(sql, params);
return pipe(
results?.rows,
// 👇 `rows` data not yet available
Either.fromNullable(() => new MissingData()),
Either.flatMap(
flow(
Schema.decodeEither(Schema.Array(schema)),
// 👇 Invalid encoded data
Either.mapLeft((parseError) => new InvalidData({ parseError }))
)
)
);
};Instead of exporting an Either we can provide a more ergonomic API to components:
useQueryEffect: hook that exportEitheruseQuery: unwrapEitherto aloading/data/error/emptyAPI
export const useQuery = <A, I>(
...args: Parameters<typeof useQueryEffect<A, I>>
) => {
const results = useQueryEffect(...args);
return Either.match(results, {
onLeft: (_) =>
Match.value(_).pipe(
Match.tagsExhaustive({
InvalidData: ({ parseError }) => ({
error: parseError,
loading: false as const,
data: undefined,
empty: false as const,
}),
MissingData: (_) => ({
loading: true as const,
data: undefined,
error: undefined,
empty: false as const,
}),
})
),
onRight: (rows) => ({
data: rows,
loading: false as const,
error: undefined,
empty: rows.length === 0,
}),
});
};With this all we need to make the reactive hooks type-safe is defining and adding a Schema for each query:
export class FoodSelect extends Schema.Class<FoodSelect>("FoodSelect")({
id: PrimaryKeyIndex,
name: Schema.NonEmptyString,
brand: Schema.NullOr(Schema.NonEmptyString),
calories: FloatQuantityInsert,
carbohydrates: FloatQuantityInsert,
proteins: FloatQuantityInsert,
fats: FloatQuantityInsert,
fatsSaturated: FloatQuantityOrUndefined,
salt: FloatQuantityOrUndefined,
fibers: FloatQuantityOrUndefined,
sugars: FloatQuantityOrUndefined,
}) {}export const useFoods = () => {
return useQuery(
(orm) => orm.select().from(foodTable).toSQL(),
FoodSelect, // 👈 Add schema
);
};With this all the hooks using useQuery are type-safe and fully validated.
We can handle missing or invalid data by extracting the value from
Eitherreturned by the hook.
