Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

openapi-react-query: Add easy way of invalidating queries #1806

Open
iffa opened this issue Aug 2, 2024 · 18 comments
Open

openapi-react-query: Add easy way of invalidating queries #1806

iffa opened this issue Aug 2, 2024 · 18 comments
Assignees
Labels
enhancement New feature or request openapi-react-query Relevant to openapi-react-query PRs welcome PRs are welcome to solve this issue!

Comments

@iffa
Copy link

iffa commented Aug 2, 2024

There is currently no easy way to invalidate queries. #1804 and #1805 together would solve this by allowing us to do it ourselves, but a built-in mechanism could also be helpful.

@michalfedyna
Copy link
Contributor

@kerwanp I'll work on that if you want, this feature will help me a lot.

@michalfedyna
Copy link
Contributor

@iffa after taking closer look, I don't think you'll need ability to inject QueryClient. Methods from openapi-react-query are using QueryClient provided by QueryClientProvider.

import { QueryClientProvider, QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient();

const QueryProvider  = ({ children }) => {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

export {QueryProvider, queryClient}

Just wrap your app with QueryProvider, and than invalidate using queryClient. It works for me.

@iffa
Copy link
Author

iffa commented Aug 2, 2024

@iffa after taking closer look, I don't think you'll need ability to inject QueryClient. Methods from openapi-react-query are using QueryClient provided by QueryClientProvider.

import { QueryClientProvider, QueryClient } from "@tanstack/react-query";



const queryClient = new QueryClient();



const QueryProvider  = ({ children }) => {

  return (

    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>

  );

};



export {QueryProvider, queryClient}

Just wrap your app with QueryProvider, and than invalidate using queryClient. It works for me.

Ah, nice. Just need an easier way to get the generated query keys and this is a good approach

@michalfedyna
Copy link
Contributor

@iffa Could you provide an example of how interface for that should look like?

@iffa
Copy link
Author

iffa commented Aug 2, 2024

@iffa Could you provide an example of how interface for that should look like?

Not sure if it is an ideal approach but one way would be to make the use* functions return the original result + the generated querykey, like this:

      return {queryKey: [...], ...useQuery({
        queryKey: [method, path, init],
        queryFn: async () => {
          const mth = method.toUpperCase() as keyof typeof client;
          const fn = client[mth] as ClientMethod<Paths, typeof method, Media>;
          const { data, error } = await fn(path, init as any); // TODO: find a way to avoid as any
          if (error || !data) {
            throw error;
          }
          return data;
        },
        ...options,
      })};

But one may also want to invalidate based on the first 2 parts of the query key (method, path) so I dunno how that would work

@michalfedyna
Copy link
Contributor

@iffa I think it should be done in @tanstack/react-query, not here. When you call useQuery from openapi-react-query you're providing keys there, so you have access to them. You could always run queryClient.clear() to clear all caches or just refetch on particular query. So in my opinion it shouldn't be done like in your example.

@iffa
Copy link
Author

iffa commented Aug 2, 2024

@iffa I think it should be done in @tanstack/react-query, not here. When you call useQuery from openapi-react-query you're providing keys there, so you have access to them. You could always run queryClient.clear() to clear all caches or just refetch on particular query. So in my opinion it shouldn't be done like in your example.

Maybe something along the lines of this:

export type GetQueryKeyMethod<
  Paths extends Record<string, Record<HttpMethod, {}>>,
> = <
  Method extends HttpMethod,
  Path extends PathsWithMethod<Paths, Method>,
  Init extends FetchOptions<FilterKeys<Paths[Path], Method>> | undefined,
>(
  method: Method,
  url: Path,
  init?: Init,
) => ReadonlyArray<unknown>;

export interface OpenapiQueryClient<
  Paths extends {},
  Media extends MediaType = MediaType,
> {
  useQuery: UseQueryMethod<Paths, Media>;
  getQueryKey: GetQueryKeyMethod<Paths>;
  useSuspenseQuery: UseSuspenseQueryMethod<Paths, Media>;
  useMutation: UseMutationMethod<Paths, Media>;
}

export default function createClient<
  Paths extends {},
  Media extends MediaType = MediaType,
>(client: FetchClient<Paths, Media>): OpenapiQueryClient<Paths, Media> {
  return {
    getQueryKey: (method, path, init) => {
      return [method, path, ...(init ? [init] : [])] as const;
    },
// ... rest

@michalfedyna
Copy link
Contributor

I think it looks better just as a helper function. I think you can open PR with that and discuss that with maintainers @drwpow @kerwanp

@kerwanp
Copy link
Contributor

kerwanp commented Aug 2, 2024

Hey! We clearly have to find a way to give the ability to easily invalidate queries.
Unfortunately, simply returning the query keys from the hook is not enough as we might want to invalidate queries outside a component (or a hook).

I think the idea of an helper function works great, your example should work @iffa. I think the function could even be defined outside the createClient so it can be used when generating the real query keys.

An other idea I had in mind was to abstract even more around tanstack:

const usePostsQuery = $api.createQuery('/api/posts/{id}');
const useCreatePostMutation = $api.createMutation('/api/posts/{id}');

export const Example1 = () => {
    const { query } = usePostsQuery();
    const { mutate } = useCreatePostMutation(data, {
        onSuccess: {
            queryClient.invalidateQueries({ queryKey: usePostsQuery.queryKey })
        }
    });
}

export const Example2 = () => {
    const { query } = usePostsQuery();
    const { mutate } = useCreatePostMutation(data, {
        onSuccess: () => {
            usePostsQuery.invalidate()
        }
    });
}

We could then imagine some kind of factory to work with CRUDS:

const $posts = $api.createCrud({
    create: '/api/posts',
    get: '/api/posts/{id}',
    delete: '/api/posts/{id}'
});

export const Example = () => {
    const { query } = $posts.useGet(5);
    const { mutate } = $posts.useCreate();
}

With optimistic updates, automatic invalidation, etc.
But this would require to move away from tanstack and be more than just a wrapper. Maybe in a separate library.

@michalfedyna
Copy link
Contributor

In my opinion openapi-react-query should be just thin wrapper around react-query providing type safety.

@kerwanp
Copy link
Contributor

kerwanp commented Aug 2, 2024

In my opinion openapi-react-query should be just thin wrapper around react-query providing type safety.

I agree with that, so let's go with @iffa example.
We just need to remove the context of query as keys can also be used in mutations. More like:

$api.keys('get', '/api/posts');
$api.getKeys('get', '/api/posts');
$api.generateKeys('get', '/api/posts');

@kerwanp kerwanp added enhancement New feature or request openapi-react-query Relevant to openapi-react-query PRs welcome PRs are welcome to solve this issue! labels Aug 2, 2024
@Pagebakers
Copy link

React Query has a queryOptions helper ideally we can do the following:

const options = queryOptions('get', '/endpoint', {})

queryClient.invalidateQueries(options)

// mutate cache
queryClient.setQueryData(options, (prevData) => ({
...prevData
})

// and this one is important to be able to fully use useSuspenseQuery
queryClient.prefetchQuery(options)

@Pagebakers
Copy link

Added this to my project, working nicely.

import {
  queryOptions as tanstackQueryOptions,
  QueryOptions,
  DefinedInitialDataOptions,
} from '@tanstack/react-query'

import type {
  ClientMethod,
  FetchResponse,
  MaybeOptionalInit,
  Client as FetchClient,
} from 'openapi-fetch'
import type {
  HttpMethod,
  MediaType,
  PathsWithMethod,
} from 'openapi-typescript-helpers'

import { getClient } from './api'

type QueryOptionsMethod<
  Paths extends Record<string, Record<HttpMethod, {}>>,
  Media extends MediaType = MediaType,
> = <
  Method extends HttpMethod,
  Path extends PathsWithMethod<Paths, Method>,
  Init extends MaybeOptionalInit<Paths[Path], Method>,
  Response extends Required<FetchResponse<Paths[Path][Method], Init, Media>>,
  Options extends Omit<
    QueryOptions<Response['data'], Response['error']>,
    'queryKey' | 'queryFn'
  >,
>(
  method: Method,
  path: Path,
  init?: Init & { [key: string]: unknown },
  options?: Options,
) => DefinedInitialDataOptions<Response['data'], Response['error']>

interface QueryOptionsReturn<
  Paths extends {},
  Media extends MediaType = MediaType,
> {
  queryOptions: QueryOptionsMethod<Paths, Media>
}

const createQueryOptions = <
  Paths extends {},
  Media extends MediaType = MediaType,
>(
  client: FetchClient<Paths, Media>,
): QueryOptionsReturn<Paths, Media> => {
  return {
    queryOptions: (method, path, init, options) => {
      return tanstackQueryOptions({
        queryKey: [method, path, init],
        queryFn: async () => {
          const mth = method.toUpperCase() as keyof typeof client
          const fn = client[mth] as ClientMethod<Paths, typeof method, Media>
          const { data, error } = await fn(path, init as any) // TODO: find a way to avoid as any
          if (error || !data) {
            throw error
          }
          return data
        },
        ...(options as any),
      })
    },
  }
}

export const { queryOptions } = createQueryOptions(getClient())

export type InferQueryData<T> =
  T extends DefinedInitialDataOptions<infer D, any> ? Partial<D> : never

@kerwanp
Copy link
Contributor

kerwanp commented Aug 18, 2024

React Query has a queryOptions helper ideally we can do the following:

const options = queryOptions('get', '/endpoint', {})

queryClient.invalidateQueries(options)

// mutate cache
queryClient.setQueryData(options, (prevData) => ({
...prevData
})

// and this one is important to be able to fully use useSuspenseQuery
queryClient.prefetchQuery(options)

I like a lot this solution as it follows the api of @tanstack/react-query.

$api.queryOptions('get', '/api/posts');

This should also make the implementation of the following issues much easier and cleaner:

@ZsugaBubu the proposed solution is really similar to your PR, do you want to take this issue?

@zsugabubus
Copy link
Contributor

zsugabubus commented Aug 20, 2024

queryOptions would cover exact matching usecase only, that is a serious limitation compared to the API provided by react-query, and it returns query options, not query filters, so theoretically it could not even be used in this context.

What about creating top-level (outside createClient) queryFilters and mutationFilters?

// { queryKey: ['get', '/items/{id}'], stale: true }
queryFilters<Paths>('get', '/items/{id}', undefined, { stale: true }));

// { queryKey: ['get', '/items/{id}', { params: ... }] }
queryFilters<Paths>('get', '/items/{id}', { params: { path: { id: 'foo' } } });

// { queryKey: ['get', '/items/{id}'], predicate: () => ... }
queryFilters<Paths>('get', '/items/{id}', init => init.params.path.id == 'foo');

// And use it like:
queryClient.invalidateQueries(queryFilters<Paths>(...));

@Pagebakers
Copy link

@zsugabubus the init and options param is optional, so you can also support non exact matching queries.

@drwpow drwpow self-assigned this Nov 27, 2024
kevmo314 added a commit to kevmo314/openapi-typescript that referenced this issue Dec 20, 2024
For urls that do not include an init param, generate a query key of length 2. This allows the value to be passed directly to `invalidateQueries()`.

Without this change, the package generates `["get", "/foo", undefined]` which does not correctly match the corresponding get query.

Related to openapi-ts#1806
kevmo314 added a commit to kevmo314/openapi-typescript that referenced this issue Dec 20, 2024
For urls that do not include an init param, generate a query key of length 2. This allows the value to be passed directly to `invalidateQueries()`.

Without this change, the package generates `["get", "/foo", undefined]` which does not correctly match the corresponding get query.

Related to openapi-ts#1806
drwpow pushed a commit that referenced this issue Jan 25, 2025
* Drop init argument when not needed in query key

For urls that do not include an init param, generate a query key of length 2. This allows the value to be passed directly to `invalidateQueries()`.

Without this change, the package generates `["get", "/foo", undefined]` which does not correctly match the corresponding get query.

Related to #1806

* Create sour-steaks-double.md
@evrrnv
Copy link

evrrnv commented Jan 27, 2025

I want to update the cache using setQueryData, my application will be heavely using setQueryData and I'm not sure about the best way to get the keys, is getting them from queryOptions is the recommended way?

@Pagebakers
Copy link

That's one of the use cases yes :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request openapi-react-query Relevant to openapi-react-query PRs welcome PRs are welcome to solve this issue!
Projects
None yet
Development

No branches or pull requests

7 participants