psteinroe / supabase-cache-helpers

A collection of framework specific Cache utilities for working with Supabase.
https://supabase-cache-helpers.vercel.app
MIT License
504 stars 32 forks source link

React Query Support #57

Closed psteinroe closed 1 year ago

psteinroe commented 2 years ago

This issue is for tracking react query support. Since I have zero experience with react-query, it would be great to find contributors to collaborate with.

psteinroe commented 2 years ago

from https://github.com/psteinroe/supabase-cache-helpers/issues/53

Curious on your approach to how you created the cache keys? React Query out of the box will infer the type of the Supabase client query, but the cache keys are still totally up in the air. I was hoping there would be a way to infer the cache keys from the query itself, but it's complex. The URL params that are sent to the rest client would be awesome, but not sure how to get them within the request, or make some sort of generic component that could do it all. I asked about doing this in a round about way https://github.com/supabase/supabase/discussions/9530#discussioncomment-3978819.

psteinroe commented 2 years ago

from #53

Curious on your approach to how you created the cache keys? React Query out of the box will infer the type of the Supabase client query, but the cache keys are still totally up in the air. I was hoping there would be a way to infer the cache keys from the query itself, but it's complex. The URL params that are sent to the rest client would be awesome, but not sure how to get them within the request, or make some sort of generic component that could do it all. I asked about doing this in a round about way supabase/supabase#9530 (reply in thread).

the query key can be easily generated using the PostgrestParser class: https://github.com/psteinroe/supabase-cache-helpers/blob/main/packages/postgrest-filter/src/postgrest-parser.ts

You can use the SWR implementation as a reference, where a PostgrestParser is created from the PostgrestFilterBuilder https://github.com/psteinroe/supabase-cache-helpers/blob/cc96081d0aa52944d3ba7ac2c2096cba634a4a9d/packages/postgrest-swr/src/lib/middleware.ts#L28

The key (which is just a string for SWR), is then simply a concatenation of all relevant properties https://github.com/psteinroe/supabase-cache-helpers/blob/cc96081d0aa52944d3ba7ac2c2096cba634a4a9d/packages/postgrest-swr/src/lib/encode.ts#L6-L22

I would be very happy to collaborate here!

psteinroe commented 2 years ago

I think the main difference between swr and react-query is the lack of middleware in the latter. In the swr implementation, the middleware takes care of encoding the key from the PostgrestFilterBuilder instance into a string while forwarding the instance to the fetcher. For react-query, it does not seem like its possible to use a different key.

I guess something like this should work, where createFetcher is imported from postgrest-fetcher and PostgrestParser from postgrest-filter.

useQuery(encode(new PostgrestParser(query)), createFetcher({...})(query))

encode could look like this

export const encode = < 
   Schema extends GenericSchema, 
   Table extends Record<string, unknown>, 
   Result 
 >( 
   parser: PostgrestParser<Schema, Table, Result> 
 ) => { 
   return [ 
     KEY_PREFIX, 
     parser.schema ?? DEFAULT_SCHEMA_NAME, 
     parser.table, 
     parser.queryKey, 
     parser.bodyKey ?? null, 
     parser.count ?? null, 
     parser.isHead ?? null, 
   ]; 
 }; 
drewbietron commented 2 years ago

@psteinroe - Awesome! Have been poking around with using just the PostgresParser to generate the cache keys only, and passing that into React Query as is and it looks promising. So a setup is looking like this.

import { useQuery } from "@tanstack/react-query";
import { client } from "@supabase/supabase-js";
import { PostgrestParser } from "@supabase-cache-helpers/postgrest-filter";

function example() {
  const query = client.from("table").select("id");
  const queryKey = new PostgrestParser(query).queryKey;
  const table = useQuery([queryKey], async () => query);
}

Abstracting this into another function that can be reused across the app is a hurdle since it's running into inferring the types from the client. Eg

import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { PostgrestParser } from "@supabase-cache-helpers/postgrest-filter";

// query arg inferred as any 😐 
function generalUseQuery(query, options?: UseQueryOptions) {
  const queryKey = new PostgrestParser(query).queryKey;

  return useQuery([queryKey], async () => query, options);
}

// This would be awesome cause the client could just use
const reactQueryData = generalUseQuery(client.from("table").select("column"));

Another note - I'm noticing that PostgresParser wont accept a single() method on a query, and that the main cache helper using SWR passes in those methods as a string argument to your query instead of letting you tack them onto the query as if you're using the Supabase client out of the box. I've been using single() in lots of places and not a big deal to refactor, but the main cache helper supporting 'single' and the parser not seems to be forcing me to not use single in the queries.

psteinroe commented 2 years ago

Hey @drewbietron,

thanks for looking into this. I did a quick draft but still get some type errors. To fix it, I will need a better understanding of how react query works and especially how the returned types are computed. But maybe it helps you. Note that this is an ugly draft and eg useCallback is missing.

import {
  useQuery as useReactQuery,
  UseQueryOptions as UseReactQueryOptions,
  UseQueryResult as UseReactQueryResult,
  QueryFunction as ReactQueryFunction,
  QueryKey,
} from "@tanstack/react-query";
import {
  PostgrestFilterBuilder,
  PostgrestError,
  PostgrestResponse,
} from "@supabase/postgrest-js";

import {
  createFetcher,
  FetcherType,
  PostgrestFetcherResponse,
} from "@supabase-cache-helpers/postgrest-fetcher";

import { GenericSchema } from "@supabase/postgrest-js/dist/module/types";
import { encode } from "../lib";
import { PostgrestParser } from "@supabase-cache-helpers/postgrest-filter";

/**
 * Perform a postgrest query
 * @param query
 * @param mode
 * @param config
 */
function useQuery<
  Schema extends GenericSchema,
  Table extends Record<string, unknown>,
  Result
>(
  query: PostgrestFilterBuilder<Schema, Table, Result> | null,
  mode: "single",
  config?: UseReactQueryOptions<
    PostgrestFetcherResponse<Result>,
    PostgrestError
  >
): UseReactQueryResult<Result, PostgrestError>;
function useQuery<
  Schema extends GenericSchema,
  Table extends Record<string, unknown>,
  Result
>(
  query: PostgrestFilterBuilder<Schema, Table, Result> | null,
  mode: "maybeSingle",
  config?: UseReactQueryOptions<
    PostgrestFetcherResponse<Result | undefined>,
    PostgrestError
  >
): UseReactQueryResult<Result | undefined, PostgrestError>;
function useQuery<
  Schema extends GenericSchema,
  Table extends Record<string, unknown>,
  Result
>(
  query: PostgrestFilterBuilder<Schema, Table, Result> | null,
  mode: "multiple",
  config?: UseReactQueryOptions<
    PostgrestFetcherResponse<Result | undefined>,
    PostgrestError
  >
): UseReactQueryResult<Result[], PostgrestError> &
  Pick<PostgrestResponse<Result[]>, "count">;
function useQuery<
  Schema extends GenericSchema,
  Table extends Record<string, unknown>,
  Result
>(
  query: PostgrestFilterBuilder<Schema, Table, Result> | null,
  mode: FetcherType,
  config?: UseReactQueryOptions<
    PostgrestFetcherResponse<Result | Result[] | undefined>,
    PostgrestError
  >
): UseReactQueryResult<Result | Result[] | undefined, PostgrestError> &
  Partial<Pick<PostgrestResponse<Result | Result[]>, "count">> {
  const queryFn: ReactQueryFunction<
    PostgrestFetcherResponse<Result | Result[]>,
    QueryKey
  > = ({ meta, queryKey, pageParam, signal }) =>
    createFetcher<Schema, Table, Result>(mode)(query);

  const res = useReactQuery(
    encode(new PostgrestParser(query)),
    queryFn,
    config
  );

  if (!res.data.data) return { data: res.data.data, ...res };

  const { data, count } = res.data;
  return { data, count, ...res };
}

export { useQuery };
psteinroe commented 2 years ago

encode looks like this:

import { PostgrestParser } from "@supabase-cache-helpers/postgrest-filter";
import { KEY_PREFIX } from "./constants";
import { DEFAULT_SCHEMA_NAME } from "@supabase-cache-helpers/postgrest-shared";
import { GenericSchema } from "@supabase/postgrest-js/dist/module/types";

export const encode = <
  Schema extends GenericSchema,
  Table extends Record<string, unknown>,
  Result
>(
  parser: PostgrestParser<Schema, Table, Result>
) => {
  return [
    KEY_PREFIX,
    parser.schema ?? DEFAULT_SCHEMA_NAME,
    parser.table,
    parser.queryKey,
    parser.bodyKey ?? "null",
    `count=${parser.count}`,
    `head=${parser.isHead}`,
  ];
};
psteinroe commented 2 years ago

@drewbietron I played around with the types and it essentially boils down to finding a solution to the problem I just asked on SO: https://stackoverflow.com/questions/74336828/derive-value-of-generic-from-parameter

psteinroe commented 2 years ago

Another note - I'm noticing that PostgresParser wont accept a single() method on a query, and that the main cache helper using SWR passes in those methods as a string argument to your query instead of letting you tack them onto the query as if you're using the Supabase client out of the box. I've been using single() in lots of places and not a big deal to refactor, but the main cache helper supporting 'single' and the parser not seems to be forcing me to not use single in the queries.

@drewbietron Using single and multiple for SWR allows me to correctly type the return type of useQuery without fixing the aforementioned problem. Nevertheless, I will soon release a new version of postgrest-filter, and PostgrestParser now accepts PostgrestBuilder. This allows passing queries with .single() and .multiple().

HermanNygaard commented 1 year ago

Hi @psteinroe. I'm working on supabase-query which seems like a subset of this library (with a Context wrapper to pass the client directly to useQuery/useMutation). I made the library back in April 2022 before Supabase v2 and tanstack-query was released and I'm looking to upgrade it. What is the current status on this issue? Maybe we can collaborate.

psteinroe commented 1 year ago

Hi @HermanNygaard thanks for reaching out! I would love to collaborate on this. I am currently doing a rewrite of the packages in #128 . As part of this, I plan to do a PoC for react-query. Goal is to make sure that the APIs of the shared packages work for both SWR and react-query. I would propose that I ping you once that PoC is done. You can take over then, and push the react-query implementation. What do you think?

HermanNygaard commented 1 year ago

Sounds good, looks promising! I was also wondering about the type inference issue for v2 mentioned here and in #53 , did you or @drewbietron ever figure that out?

psteinroe commented 1 year ago

@HermanNygaard yes, I did. After the rewrite, you can pass PostgrestBuilder directly and the types are inferred correct. You can track the progress in the rewrite PR. The only major box left to tick is the mutation issue. The rest are minor dx improvements. The new useQuery without the mode param is already there. 🕺

psteinroe commented 1 year ago

@HermanNygaard I published a pre-release of the react-query package yesterday. Check out the rewrite PR for details!

psteinroe commented 1 year ago

also @drewbietron would love your feedback.

psteinroe commented 1 year ago

I published @supabase-cache-helpers/react-query last night! Pagination and Infinite Scroll are still missing though. Will close this and add new issues.