Decode data with effect schema

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 a MissingData error
  • If decoding fails, it means the data is invalid and we return a InvalidData error

We wrap the schema parameter inside Schema.Array. We also use decodeEither to handle the error using Either.

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 }))
      )
    )
  );
};

With this all we need to make the reactive hooks type-safe is defining and adding a Schema for each query:

/schema/food.ts
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 Either returned by the hook.